diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml new file mode 100644 index 0000000..69175b4 --- /dev/null +++ b/.github/workflows/on-pr.yml @@ -0,0 +1,28 @@ +name: On Pull Request + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 24 + uses: actions/setup-java@v4 + with: + distribution: oracle + java-version: 24 + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build + run: mvn -q compile diff --git a/.github/workflows/test-execution.yml b/.github/workflows/test-execution.yml new file mode 100644 index 0000000..338cdd1 --- /dev/null +++ b/.github/workflows/test-execution.yml @@ -0,0 +1,33 @@ +name: Build and Test +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + local-test: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 24 + uses: actions/setup-java@v4 + with: + java-version: 24 + distribution: oracle + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build with Maven + run: mvn -q -DskipTests package + + - name: Run local tests + run: mvn -q test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome diff --git a/.github/workflows/tests-executon.yml b/.github/workflows/tests-executon.yml deleted file mode 100644 index e225e8d..0000000 --- a/.github/workflows/tests-executon.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Test execution -on: - push: - branches: - - master - pull_request: - branches: - - master -jobs: - build: - runs-on: ubuntu-latest - env: - SELENIUM_HUB_HOST: hub - services: - hub: - image: selenium/hub - firefox: - image: selenium/standalone-chrome - env: - HUB_HOST: hub - HUB_PORT: 4444 - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - name: Cache Maven packages - uses: actions/cache@v1 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Build with Maven - run: mvn -B package --file pom.xml - - name: Run the tests - run: mvn test \ No newline at end of file diff --git a/.gitignore b/.gitignore index afd597e..6cf5705 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .settings/ +grid/assets # Intellij .idea/ diff --git a/pipeline_as_code/.gitlab-ci.yml b/.sdlc/.gitlab-ci.yml similarity index 59% rename from pipeline_as_code/.gitlab-ci.yml rename to .sdlc/.gitlab-ci.yml index 2203018..bf76065 100644 --- a/pipeline_as_code/.gitlab-ci.yml +++ b/.sdlc/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: maven:3.5.4-jdk-8 +image: maven:3.8.4-openjdk-17 stages: - build @@ -17,4 +17,4 @@ build: test: stage: test script: - - mvn test -Pweb-execution -Dsuite=multi_browser -Denv=test \ No newline at end of file + - mvn test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome diff --git a/pipeline_as_code/Jenkinsfile b/.sdlc/Jenkinsfile similarity index 86% rename from pipeline_as_code/Jenkinsfile rename to .sdlc/Jenkinsfile index d0a4f6e..21899b8 100644 --- a/pipeline_as_code/Jenkinsfile +++ b/.sdlc/Jenkinsfile @@ -2,7 +2,7 @@ node { def mvnHome stage('Preparation') { - git 'https://github.com/eliasnogueira/selenium-java-bootstrap.git' + git 'https://github.com/eliasnogueira/selenium-java-lean-test-achitecture.git' mvnHome = tool 'M3' } @@ -12,7 +12,7 @@ node { stage('Test Execution') { try { - sh "'${mvnHome}/bin/mvn' test -Pweb-execution -Dsuite=multi_browser" + sh "'${mvnHome}/bin/mvn' test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome } catch (Exception e) { currentBuild.result = 'FAILURE' } finally { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa61071..21a0ef8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contribution guide ## I have an idea or I want to create an issue -If you have an idea, suggestion, feature or an issue, please log an [issue](https://github.com/eliasnogueira/selenium-java-bootstrap/issues). +If you have an idea, suggestion, feature or an issue, please log an [issue](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/issues). Select _Bug report_ if tou want log an issue or _Feature request_ if you want to see something new. @@ -11,7 +11,7 @@ Do not forget to add a _label_ on the issue or feature. Excellent! Thank you to help me out! You're going to need a few things first: -* JDK 11+ +* JDK 17+ * [Configure your IDE](https://projectlombok.org/setup/overview) in order to support Lombok. ## Send a pull request diff --git a/README.MD b/README.MD index b76d386..d90fecc 100644 --- a/README.MD +++ b/README.MD @@ -1,26 +1,45 @@ -This project delivers to you a complete lean test architecture for your web tests using the best frameworks and practices. +# Lean Test Automation Architecture using Java and Selenium WebDriver -![Local testing example](example_filed_test_with_report.gif) +[![Actions Status](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/workflows/Build%20and%20Test/badge.svg)](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/actions) + +**This project delivers to you a complete lean test architecture for your web tests using the best frameworks and +practices.** + +It has a complete solution to run tests in different ways: + +* local testing using the browser on your local machine +* parallel (or single) testing using Selenium Docker +* local testing using TestContainers +* Distributed execution using Selenium Grid + +## Examples + +### Local testing execution example + +![Local testing execution example](assets/example_filed_test_with_report.gif) + +### Parallel testing execution example with Selenium Grid + +![Parallel testing execution example with Selenium Grid](assets/selenium-grid-execution.gif) ## Languages and Frameworks -This project using the following languages and frameworks: +This project uses the following languages and frameworks: -* [Java 11](https://openjdk.java.net/projects/jdk/11/) as the programming language +* [Java 23](https://openjdk.java.net/projects/jdk/23/) as the programming language * [TestNG](https://testng.org/doc/) as the UnitTest framework to support the test creation -* [Seleium WebDriver](https://www.selenium.dev/) as the web browser automation framework using the Java binding -* [AsseertJ](https://joel-costigliola.github.io/assertj/) as the fluent assertion library +* [Selenium WebDriver](https://www.selenium.dev/) as the web browser automation framework using the Java binding +* [AssertJ](https://joel-costigliola.github.io/assertj/) as the fluent assertion library * [Allure Report](https://docs.qameta.io/allure/) as the testing report strategy -* [JavaFaker](https://github.com/DiUS/java-faker) as the faker data generation strategy -* [Log4J2](https://logging.apache.org/log4j/2.x/) as the logging manage strategy -* [WebDriverManager](https://github.com/bonigarcia/webdrivermanager) as the Selenium binaries management +* [DataFaker](https://www.datafaker.net/) as the faker data generation strategy +* [Log4J2](https://logging.apache.org/log4j/2.x/) as the logging management strategy * [Owner](http://owner.aeonbits.org/) to minimize the code to handle the properties file -* [Lombok](https://projectlombok.org/) to minimize the boilerplate in the Java code - +* [TestContainers](https://java.testcontainers.org/modules/webdriver_containers/) Webdriver Containers ## Test architecture -We know that any automation project starting with a good test architecture. +We know that any automation project starts with a good test architecture. + This project can be your initial test architecture for a faster start. You will see the following items in this architecture: @@ -34,15 +53,18 @@ You will see the following items in this architecture: * [Test Data Factory](#test-data-factory) * [Profiles executors on pom.xml](#profiles-executors-on-pomxml) * [Pipeline as a code](#pipeline-as-a-code) +* [Test environment abstraction](#execution-with-docker-selenium-distributed) -Do you have any other item to add on this test architecture? Please do a pull request or open an issue to discuss +Do you have any other items to add to this test architecture? Please do a pull request or open an issue to discuss. ### Page Objects pattern -I will not explain the Page Object pattern because you can find a lot of good explanations and examples on the internet. + +I will not explain the Page Object pattern because you can find a lot of good explanations and examples on the internet. Instead, I will explain what exactly about page objects I'm using in this project. #### AbstractPageObject -This class has a protected constructor to remove the necessity to init the elements using the Page Factory. + +This class has a protected constructor to remove the necessity to init the elements using the Page Factory. Also, it sets the timeout from the `timeout` property value located on `general.properties` file. All the Page Object classes should extend the `AbstractPageObject`. @@ -53,78 +75,152 @@ It also tries to remove the `driver` object from the Page Object class as much a > There's a `NavigationPage` on the `common` package inside the Page Objects. > Notice that all the pages extend this one instead of the `AbstractPageObject`. I implemented this way: > * because the previous and next buttons are fixed on the page (there's no refresh on the page) -> * to avoid create or pass the new reference to the `NavigationPage` when we need to hit previous or next buttons +> * to avoid creating or passing the new reference to the `NavigationPage` when we need to hit previous or next buttons As much as possible avoid this strategy to not get an `ElementNotFoundException` or `StaleElementReferenceException`. Use this approach if you know that the page does not refresh. ### Execution types -There are two execution types: **local** and **remote**. -For both, there's a factory class [DriverFactory](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/driver/DriverFactory.java) -to resolve if the execution is local or remote based on the `target ` property value located on `general.properties` file. +There are different execution types: -The class [DriverManager](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/driver/DriverManager.java) -create a `ThreadLocal` for the WebDriver instance, to make sure there's no conflict when we run it in parallel. +- `local` +- `local-suite` +- `selenium-grid` +- `testcontainers` + +The `TargetFactory` class will resolve the target execution based on the `target` property value located +on `general.properties` file. Its usage is placed on the `BaseWeb` class before each test execution. #### Local execution -This execution type uses [WebDriverManager](https://github.com/bonigarcia/webdrivermanager) class to instantiate the web browsers. -The class [LocalDriverManager](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/driver/local/LocalDriverManager.java) -create a browser instance from the value placed on the `browser` property on the `local.properties` file. -It matches the browser name with an internal Enum type on WebDriverManager. +##### Local machine + +**This approach is automatically used when you run the test class in your IDE.** + +When the `target` is `local` the `createLocalDriver()` method is used from the `BrowserFactory` class to return the +browser instance. + +The browser used in the test is placed on the `browser` property in the `general.properties` file. + +##### Local Suite + +It's the same as the Local Execution, where the difference is that the browser is taken from the TestNG suite file +instead of the `general.properties` +file, enabling you to run multi-browser test approach locally. + +##### Testcontainers + +This execution type uses the [WebDriver Containers](https://www.testcontainers.org/modules/webdriver_containers/) in +Testcontainers to run the tests in your machine, but using the Selenium docker images for Chrome or Firefox. + +When the `target` is `testcontainers` the `TargetFactory` uses the `createTestContainersInstance()` method to initialize +the container based on the browser set in the `browser` property. Currently, Testcontainers only supports Chrome and +Firefox. + +Example + +```shell +mvn test -Pweb-execution -Dtarget=testcontainers -Dbrowser=chrome +``` #### Remote execution -This execution type uses [RemoteDriverManager](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/driver/remote/RemoteDriverManager.java) -class to connect on the remote grid and create the browser instance. -The approach of the remote execution here was created to enable the parallel execution, but if you set the `target` -property value to `grid` and run a test it will work because if there's no explicit browser in use, Google Chrome will be used. -You can check this on the [BaseWeb](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/test/BaseWeb.java) class. +##### Selenium Grid + +The Selenium Grid approach executes the tests in remote machines (local or remote/cloud grid). +When the `target` is `selenium-grid` the `getOptions` method is used from the `BrowserFactory` to return the browser +option +class as the remote execution needs the browser capability. -You must pay attention to the two required information regarding the remote execution: the `grid.url` and `grid.port` +The `DriverFactory` class has an internal method `createRemoteInstance` to return a `RemoteWebDriver` instance based on +the browser capability. + +You must pay attention to the two required information regarding the remote execution: the `grid.url` and `grid.port` property values on the `grid.properties` file. You must update these values before the start. -If you are using the `docker-compose.yml` file to start the Zalenium grid, the values on the `grid.property` file should work. +If you are using the `docker-compose.yml` file to start the Docker Selenium grid, the values on the `grid.properties` +file should work. + +You can take a look at the [Execution with Docker Selenium Distributed](#execution-with-docker-selenium-distributed) +to run the parallel tests using this example. + +#### BrowserFactory class -Please take a look at the [Parallel Execution](#parallel-execution) section. +This Factory class is a Java enum that has all implemented browsers to use during the test execution. +Each browser is an `enum`, and each enum implements four methods: + +* `createLocalDriver()`: creates the browser instance for the local execution. The browser driver is automatically + managed by the WebDriverManager library +* `createDriver()`: creates the browser instance for the remote execution +* `getOptions()`: creates a new browser `Options` setting some specific configurations, and it's used for the remote + executions using the Selenium Grid +* `createTestContainerDriver()` : Creates selenium grid lightweight test container in Standalone mode with + Chrome/Firefox/Edge browser support. + +You can see that the `createLocalDriver()` method use the `getOptions()` to get specific browser configurations, as +starting the browser maximized and others. + +The `getOptions()` is also used for the remote execution as it is a subclass of the `AbstractDriverOptions` and can be +automatically accepted as either a `Capabilities` or `MutableCapabilities` class, which is required by +the `RemoteWebDriver` class. + +#### DriverManager class + +The +class [DriverManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/driver/DriverManager.java) +create a `ThreadLocal` for the WebDriver instance, to make sure there's no conflict when we run it in parallel. ### BaseTest -This testing pattern was implemented on the [BaseWeb](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/test/BaseWeb.java) + +This testing pattern was implemented on +the [BaseWeb](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/test/java/com/eliasnogueira/BaseWeb.java) class to automatically run the pre (setup) and post (teardown) conditions. -The pre-condition uses `@BeforeMethod` from TestNG creates the browser instance based on the values passed either local or remote execution. +The pre-condition uses `@BeforeMethod` from TestNG creates the browser instance based on the values passed either local +or remote execution. The post-condition uses `@AfterMethod` to close the browser instance. Both have the `alwaysRun` parameter as `true` to force the run on a pipeline. -Pay attention that it was designed to open a browser instance to each `@Test` located on the test class. +Pay attention that it was designed to open a browser instance to each `@Test` located in the test class. -This class also the `TestListener` that is a custom TestNG listener, and will be described in the next section. +This class also has the `TestListener` annotation which is a custom TestNG listener, and will be described in the next +section. ### TestListener -The `TestListener` is a class that implements [ITestListener](https://testng.org/doc/documentation-main.html#logging-listeners). -The following method is in use to help logging errors and attach additional information on the test report: -* `onTestStart`: add the browser information into the test report -* `onTestFailure`: log the exceptions and add a screenshot on the test report -* `onTestSkipped`: add the skipped test on the log +The `TestListener` is a class that +implements [ITestListener](https://testng.org/doc/documentation-main.html#logging-listeners). +The following method is used to help logging errors and attach additional information to the test report: + +* `onTestStart`: add the browser information to the test report +* `onTestFailure`: log the exceptions and add a screenshot to the test report +* `onTestSkipped`: add the skipped test to the log ### Logging + All the log is done by the Log4J using the `@Log4j2` annotation. The `log4j2.properties` has two strategies: console and file. -A file with all the log information will be automatically created on the user folder with `test_automation.log` filename. +A file with all the log information will be automatically created on the user folder with `test_automation.log` +filename. If you want to change it, update the `appender.file.fileName` property value. -The `log.error` is used to log all the exceptions this architecture might throw. Use `log.info` or `log.debug` to log -important information, like the users automatically generated by the factory [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/data/BookingDataFactory.java) +The `log.error` is used to log all the exceptions this architecture might throw. Use `log.info` or `log.debug` to log +important information, like the users, automatically generated by the +factory [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) ### Parallel execution -The parallel test execution is based on the [parallel tests](https://testng.org/doc/documentation-main.html#parallel-tests) -feature on TestNG. This is in use on the `multi_browser.xml` test suite file by the `parallel="tests"` attribute and value, -which means each `test` item inside the test suite will execute in parallel. + +The parallel test execution is based on +the [parallel tests](https://testng.org/doc/documentation-main.html#parallel-tests) +feature on TestNG. This is used by `selenium-grid.xml` test suite file which has the `parallel="tests"` attribute and +value, +whereas `test` item inside the test suite will execute in parallel. The browser in use for each `test` should be defined by a parameter, like: + ```xml + ``` @@ -132,79 +228,125 @@ You can define any parallel strategy. It can be an excellent combination together with the grid strategy. +#### Execution with Docker Selenium Distributed + +This project has the `docker-compose.yml` file to run the tests in a parallel way using Docker Selenium. +To be able to run it in parallel the file has +the [Dynamic Grid Implementation](https://github.com/SeleniumHQ/docker-selenium#dynamic-grid-) that will start the +container on demand. + +This means that Docker Selenium will start a container test for a targeting browser. + +Please note that you need to do the following actions before running it in parallel: + +* Docker installed +* Pull the images for Chrome Edge and Firefox - Optional + * Images are pulled if not available and initial test execution will be slow + * `docker pull selenium-standalog-chrome` + * `docker pull selenium-standalog-firefox` + * `docker pull selenium/standalone-edge` + * If you are using a MacBook with either M1 or M2 chip you must check the following experimental feature in Docker + Desktop: Settings -> Features in development -> Use Rosetta for x86/amd64 emulation on Apple Silicon +* Pay attention to the `grid/config.toml` file that has comments for each specific SO +* Start the Grid by running the following command inside the `grid` folder + * `docker-compose up` +* Run the project using the following command + +```shell +mvn test -Pweb-execution -Dsuite=selenium-grid -Dtarget=selenium-grid -Dheadless=true +``` + +* Open the [Selenium Grid] page to see the node status + ### Configuration files -This project uses a library called [Owner](http://owner.aeonbits.org/). You can find the class related to the property + +This project uses a library called [Owner](http://owner.aeonbits.org/). You can find the class related to the property file reader in the following classes: -* [Configuration](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/config/Configuration.java) -* [ConfigurationManager](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/config/ConfigurationManager.java) -There are 3 properties (configuration) files located on `src/main/java/resources/conf`: -* `general.properties`: general configuration as the target execution, base url, timeout, and faker locale -* `grid.properties`: url and port for the Selenium grid usage (Zalenium) -* `local.properties`: browser to use in the local execution +* [Configuration](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/config/Configuration.java) +* [ConfigurationManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/config/ConfigurationManager.java) + +There are 3 properties (configuration) files located on `src/test/java/resources/`: -The properties were divided into three different ones to better separate the responsibilities and enable the changes easy -without have a lot of properties inside a single file. +* `general.properties`: general configuration as the target execution, browser, base url, timeout, and faker locale +* `grid.properties`: url and port for the Selenium grid usage + +The properties were divided into three different ones to better separate the responsibilities and enable the changes +easy without having a lot of properties inside a single file. ### Test Data Factory + Is the utilization of the Factory design pattern with the Fluent Builder to generate dynamic data. -The [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/data/BookingDataFactory.java) -has only one factory `createBookingData` returning a `Booking` object with dinamyc data. +The [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) +has only one factory `createBookingData` returning a `Booking` object with dynamic data. This dynamic data is generated by JavaFaker filling all the fields using the Build pattern. -The [Booking](https://github.com/eliasnogueira/selenium-java-bootstrap/blob/master/src/main/java/model/Booking.java) -the class uses Lombok to reduce the boilerplate, easily providing getters, setters, constructors, and the build pattern. +The [Booking](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/Booking.java) +is the plain Java objects +and +the [BookingBuilder](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/BookingBuilder.java) +is the builder class. + +You can see the usage of the Builder pattern in +the [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) +class. Reading reference: https://reflectoring.io/objectmother-fluent-builder ### Profiles executors on pom.xml -There is a profile called _multi-browser_ created to execute the test suite _multi_browser.xml_ inside _src/test/resources/suites_ folder. -To execute this suite, via command line you can call the parameter `-P` and the profile id. +There is a profile called `web-execution` created to execute the test suite `local.xml` +inside `src/test/resources/suites` folder. +To execute this suite, via the command line you can call the parameter `-P` and the profile id. Eg: executing the multi_browser suite + ``` bash -mvn test -Pmulti-browser +mvn test -Pweb-execution ``` If you have more than one suite on _src/test/resources/suites_ folder you can parameterize the xml file name. To do this you need: -* Create a property on pom.xml called _suite_ +* Create a property on `pom.xml` called _suite_ ```xml - - multi_browser - + + + local + ``` * Change the profile id ```xml + - web_execution + web-execution ``` * Replace the xml file name to `${suite}` on the profile ```xml + - - src/test/resources/suites/${suite}.xml - + + src/test/resources/suites/${suite}.xml + ``` * Use `-Dsuite=suite_name` to call the suite ````bash -mvn test -Pweb_execution -Dsuite=multi_browser +mvn test -Pweb-execution -Dsuite=suite_name ```` ### Pipeline as a code -The two files of pipeline as a code are inside `pipeline_as_code` folder. +The two files of the pipeline as a code are inside `pipeline_as_code` folder. -* Jenkins: `Jenkinsfile` to be used on a Jenkins pipeline -* GitLab CI: `.gitlab-ci.yml` to be used on a GitLab CI \ No newline at end of file +* GitHub Actions to use it inside the GitHub located at `.github\workflows` +* Jenkins: `Jenkinsfile` to be used on a Jenkins pipeline located at `pipeline_as_code` +* GitLab CI: `.gitlab-ci.yml` to be used on a GitLab CI `pipeline_as_code` diff --git a/example_filed_test_with_report.gif b/assets/example_filed_test_with_report.gif similarity index 100% rename from example_filed_test_with_report.gif rename to assets/example_filed_test_with_report.gif diff --git a/assets/selenium-grid-execution.gif b/assets/selenium-grid-execution.gif new file mode 100644 index 0000000..67d0c29 Binary files /dev/null and b/assets/selenium-grid-execution.gif differ diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2b634ac..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ - # Usage: - # docker-compose up --force-recreate - version: '2.1' - - services: - #--------------# - zalenium: - image: "dosel/zalenium" - container_name: zalenium - hostname: zalenium - tty: true - volumes: - - /tmp/videos:/home/seluser/videos - - /var/run/docker.sock:/var/run/docker.sock - ports: - - 4444:4444 - command: > - start --desiredContainers 2 - --maxDockerSeleniumContainers 10 - --maxTestSessions 4 - --screenWidth 1280 --screenHeight 1024 - --timeZone "Europe/Amsterdam" - --videoRecordingEnabled false - environment: - ZALENIUM_NO_PROXY: localhost,127.0.0.1 \ No newline at end of file diff --git a/grid/config.toml b/grid/config.toml new file mode 100644 index 0000000..2d961ae --- /dev/null +++ b/grid/config.toml @@ -0,0 +1,28 @@ +[docker] +# Configs have a mapping between the Docker image to use and the capabilities that need to be matched to +# start a container with the given image. +configs = [ + "selenium/standalone-firefox:latest", "{\"browserName\": \"firefox\"}", + "selenium/standalone-chrome:latest", "{\"browserName\": \"chrome\"}" + ] + +host-config-keys = ["Binds"] + +# URL for connecting to the docker daemon +# Most simple approach, leave it as http://127.0.0.1:2375, and mount /var/run/docker.sock. +# 127.0.0.1 is used because internally the container uses socat when /var/run/docker.sock is mounted +# If var/run/docker.sock is not mounted: +# Windows: make sure Docker Desktop exposes the daemon via tcp, and use http://host.docker.internal:2375. +# macOS: install socat and run the following command, socat -4 TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock, +# then use http://host.docker.internal:2375. +# Linux: varies from machine to machine, please mount /var/run/docker.sock. If this does not work, please create an issue. + +url = "http://host.docker.internal:2375/" +# Docker image used for video recording +video-image = "selenium/video:latest" + +# Uncomment the following section if you are running the node on a separate VM +# Fill out the placeholders with appropriate values +#[server] +#host = +#port = diff --git a/grid/docker-compose.yml b/grid/docker-compose.yml new file mode 100644 index 0000000..7163cac --- /dev/null +++ b/grid/docker-compose.yml @@ -0,0 +1,22 @@ +services: + node-docker: + image: selenium/node-docker:latest + volumes: + - ./assets:/opt/selenium/assets + - ./config.toml:/opt/bin/config.toml + - ~/Downloads:/home/seluser/Downloads + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + + selenium-hub: + image: selenium/hub:latest + container_name: selenium-hub + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" diff --git a/pom.xml b/pom.xml index e101705..d3faf76 100644 --- a/pom.xml +++ b/pom.xml @@ -5,31 +5,41 @@ 4.0.0 com.eliasnogueira - selenium-java-bootstrap - 1.4 + selenium-java-lean-test-architecture + 3.8.0 + + + scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git + scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git + + + 24 UTF-8 UTF-8 - 11 - 2.22.2 - 3.8.1 - - 1.9.5 - 4.0.0-alpha-6 - 4.0.0-alpha-2 - 7.1.0 - 3.16.1 - 1.0.2 - 2.13.3 - 1.18.12 - 4.0.0 + 3.5.3 + 3.14.0 + + 1.9.24 + 4.35.0 + 7.11.0 + 3.27.4 + 2.4.4 + 2.25.1 1.0.12 - 2.13.3 - 2.10.0 + 2.33.0 + 2.29.1 + 2.29.1 + 2.15.2 1.0.0 + + https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline + + 1.21.3 + 2.0.17 - multi_browser + local @@ -39,12 +49,6 @@ ${selenium.version} - - org.seleniumhq.selenium - selenium-server - ${selenium-server.version} - - org.testng testng @@ -58,9 +62,9 @@ - com.github.javafaker - javafaker - ${javafaker.version} + net.datafaker + datafaker + ${datafaker.version} @@ -76,10 +80,9 @@ - org.projectlombok - lombok - ${lombok.version} - provided + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} @@ -88,33 +91,52 @@ ${owner.version} - - io.github.bonigarcia - webdrivermanager - ${webdrivermanager.version} - - io.qameta.allure allure-testng - ${allure.version} + ${allure-testng.version} io.qameta.allure allure-attachments - ${allure.version} + ${allure-attachments.version} com.github.automatedowl allure-environment-writer ${allure-environment-writer.version} + + + com.google.guava + guava + + + + + + org.testcontainers + selenium + ${testcontainers.selenium.version} + + + org.apache.commons + commons-compress + + + + + + org.slf4j + slf4j-simple + ${slf4j-simple.version} + test - + web-execution @@ -133,6 +155,7 @@ + @@ -147,9 +170,6 @@ -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" false - - target/allure-results - @@ -163,27 +183,23 @@ io.qameta.allure allure-maven ${allure-maven.version} + + ${allure.version} + + ${allure.cmd.download.url}/${allure.version}/allure-commandline-${allure.version}.zip + + org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} - ${java-compiler.version} - ${java-compiler.version} + ${java-compiler.version} - - - src/test/resources - true - - *.properties - - - - \ No newline at end of file + diff --git a/src/main/java/config/Configuration.java b/src/main/java/com/eliasnogueira/config/Configuration.java similarity index 91% rename from src/main/java/config/Configuration.java rename to src/main/java/com/eliasnogueira/config/Configuration.java index 47e948e..dda4233 100644 --- a/src/main/java/config/Configuration.java +++ b/src/main/java/com/eliasnogueira/config/Configuration.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package config; +package com.eliasnogueira.config; import org.aeonbits.owner.Config; import org.aeonbits.owner.Config.LoadPolicy; @@ -31,9 +31,8 @@ @LoadPolicy(LoadType.MERGE) @Config.Sources({ "system:properties", - "classpath:conf/general.properties", - "classpath:conf/local.properties", - "classpath:conf/grid.properties"}) + "classpath:general.properties", + "classpath:selenium-grid.properties"}) public interface Configuration extends Config { @Key("target") @@ -49,7 +48,7 @@ public interface Configuration extends Config { String url(); @Key("timeout") - String timeout(); + int timeout(); @Key("grid.url") String gridUrl(); diff --git a/src/main/java/config/ConfigurationManager.java b/src/main/java/com/eliasnogueira/config/ConfigurationManager.java similarity index 94% rename from src/main/java/config/ConfigurationManager.java rename to src/main/java/com/eliasnogueira/config/ConfigurationManager.java index 3fd91db..ce15843 100644 --- a/src/main/java/config/ConfigurationManager.java +++ b/src/main/java/com/eliasnogueira/config/ConfigurationManager.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package config; +package com.eliasnogueira.config; import org.aeonbits.owner.ConfigCache; @@ -31,8 +31,7 @@ public class ConfigurationManager { private ConfigurationManager() { } - public static Configuration getConfiguration() { + public static Configuration configuration() { return ConfigCache.getOrCreate(Configuration.class); } } - diff --git a/src/main/java/model/Booking.java b/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java similarity index 65% rename from src/main/java/model/Booking.java rename to src/main/java/com/eliasnogueira/data/changeless/BrowserData.java index 82876b5..810154d 100644 --- a/src/main/java/model/Booking.java +++ b/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2018 Elias Nogueira + * Copyright (c) 2022 Elias Nogueira * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,28 +22,17 @@ * SOFTWARE. */ -package model; +package com.eliasnogueira.data.changeless; -import enums.RoomType; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.ToString; +public final class BrowserData { -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -public class Booking { + private BrowserData() { + } - String email; - String country; - @ToString.Exclude - String password; - String dailyBudget; - Boolean newsletter; - RoomType roomType; - String roomDescription; + public static final String START_MAXIMIZED = "--start-maximized"; + public static final String DISABLE_INFOBARS = "--disable-infobars"; + public static final String DISABLE_NOTIFICATIONS = "--disable-notifications"; + public static final String REMOTE_ALLOW_ORIGINS = "--remote-allow-origins=*"; + public static final String GENERIC_HEADLESS = "-headless"; + public static final String CHROME_HEADLESS = "--headless=new"; } diff --git a/src/main/java/data/BookingDataFactory.java b/src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java similarity index 59% rename from src/main/java/data/BookingDataFactory.java rename to src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java index 99e74c5..c1dc321 100644 --- a/src/main/java/data/BookingDataFactory.java +++ b/src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java @@ -22,51 +22,46 @@ * SOFTWARE. */ -package data; +package com.eliasnogueira.data.dynamic; + +import com.eliasnogueira.enums.RoomType; +import com.eliasnogueira.model.Booking; +import net.datafaker.Faker; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; -import com.github.javafaker.Faker; -import config.Configuration; -import config.ConfigurationManager; -import enums.RoomType; import java.util.Locale; -import java.util.Random; -import lombok.extern.log4j.Log4j2; -import model.Booking; -@Log4j2 -public class BookingDataFactory { +import static com.eliasnogueira.config.ConfigurationManager.configuration; + +public final class BookingDataFactory { - private final Faker faker; + private static final Faker faker = new Faker(new Locale.Builder().setLanguageTag(configuration().faker()).build()); + private static final Logger logger = LogManager.getLogger(BookingDataFactory.class); - public BookingDataFactory() { - Configuration configuration = ConfigurationManager.getConfiguration(); - faker = new Faker(new Locale(configuration.faker())); + private BookingDataFactory() { } - public Booking createBookingData() { - Booking booking = Booking.builder(). + public static Booking createBookingData() { + var booking = new Booking.BookingBuilder(). email(faker.internet().emailAddress()). country(returnRandomCountry()). password(faker.internet().password()). dailyBudget(returnDailyBudget()). newsletter(faker.bool().bool()). - roomType(RoomType.getRandom()). + roomType(faker.options().option(RoomType.class)). roomDescription(faker.lorem().paragraph()). build(); - log.info(booking); + logger.info(booking); return booking; } - private String returnRandomCountry() { - return returnRandomItemOnArray(new String[]{"Belgium", "Brazil", "Netherlands"}); - } - - private String returnDailyBudget() { - return returnRandomItemOnArray(new String[]{"$100", "$100 - $499", "$499 - $999", "$999+"}); + private static String returnRandomCountry() { + return faker.options().option("Belgium", "Brazil", "Netherlands"); } - private String returnRandomItemOnArray(String[] array) { - return array[(new Random().nextInt(array.length))]; + private static String returnDailyBudget() { + return faker.options().option("$100", "$100 - $499", "$499 - $999", "$999+"); } } diff --git a/src/main/java/com/eliasnogueira/driver/BrowserFactory.java b/src/main/java/com/eliasnogueira/driver/BrowserFactory.java new file mode 100644 index 0000000..0b7f43f --- /dev/null +++ b/src/main/java/com/eliasnogueira/driver/BrowserFactory.java @@ -0,0 +1,163 @@ +/* + * MIT License + * + * Copyright (c) 2021 Elias Nogueira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.eliasnogueira.driver; + +import com.eliasnogueira.exceptions.HeadlessNotSupportedException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.edge.EdgeDriver; +import org.openqa.selenium.edge.EdgeOptions; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.remote.AbstractDriverOptions; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.safari.SafariDriver; +import org.openqa.selenium.safari.SafariOptions; +import org.testcontainers.containers.BrowserWebDriverContainer; + +import static com.eliasnogueira.config.ConfigurationManager.configuration; +import static com.eliasnogueira.data.changeless.BrowserData.CHROME_HEADLESS; +import static com.eliasnogueira.data.changeless.BrowserData.DISABLE_INFOBARS; +import static com.eliasnogueira.data.changeless.BrowserData.DISABLE_NOTIFICATIONS; +import static com.eliasnogueira.data.changeless.BrowserData.GENERIC_HEADLESS; +import static com.eliasnogueira.data.changeless.BrowserData.REMOTE_ALLOW_ORIGINS; +import static com.eliasnogueira.data.changeless.BrowserData.START_MAXIMIZED; +import static java.lang.Boolean.TRUE; + +public enum BrowserFactory { + + CHROME { + @Override + public WebDriver createLocalDriver() { + return new ChromeDriver(getOptions()); + } + + @Override + public WebDriver createTestContainerDriver() { + BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new ChromeOptions()); + driverContainer.start(); + + return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new ChromeOptions()); + } + + @Override + public ChromeOptions getOptions() { + var chromeOptions = new ChromeOptions(); + chromeOptions.addArguments(START_MAXIMIZED); + chromeOptions.addArguments(DISABLE_INFOBARS); + chromeOptions.addArguments(DISABLE_NOTIFICATIONS); + chromeOptions.addArguments(REMOTE_ALLOW_ORIGINS); + + if (configuration().headless()) chromeOptions.addArguments(CHROME_HEADLESS); + + return chromeOptions; + } + }, FIREFOX { + @Override + public WebDriver createLocalDriver() { + return new FirefoxDriver(getOptions()); + } + + @Override + public WebDriver createTestContainerDriver() { + BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new FirefoxOptions()); + driverContainer.start(); + + return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new FirefoxOptions()); + } + + @Override + public FirefoxOptions getOptions() { + var firefoxOptions = new FirefoxOptions(); + firefoxOptions.addArguments(START_MAXIMIZED); + + if (configuration().headless()) firefoxOptions.addArguments(GENERIC_HEADLESS); + + return firefoxOptions; + } + }, EDGE { + @Override + public WebDriver createLocalDriver() { + return new EdgeDriver(getOptions()); + } + + public WebDriver createTestContainerDriver() { + BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new EdgeOptions()); + driverContainer.start(); + + return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new EdgeOptions()); + } + + @Override + public EdgeOptions getOptions() { + var edgeOptions = new EdgeOptions(); + edgeOptions.addArguments(START_MAXIMIZED); + + if (configuration().headless()) edgeOptions.addArguments(GENERIC_HEADLESS); + + return edgeOptions; + } + }, SAFARI { + @Override + public WebDriver createLocalDriver() { + return new SafariDriver(getOptions()); + } + + public WebDriver createTestContainerDriver() { + throw new IllegalArgumentException("Browser Safari not supported on TestContainers yet"); + } + + @Override + public SafariOptions getOptions() { + var safariOptions = new SafariOptions(); + safariOptions.setAutomaticInspection(false); + + if (TRUE.equals(configuration().headless())) + throw new HeadlessNotSupportedException(safariOptions.getBrowserName()); + + return safariOptions; + } + }; + + /** + * Used to run local tests where the WebDriverManager will take care of the driver + * + * @return a new WebDriver instance based on the browser set + */ + public abstract WebDriver createLocalDriver(); + + /** + * @return a new AbstractDriverOptions instance based on the browser set + */ + public abstract AbstractDriverOptions getOptions(); + + /** + * Used to run the remote test execution using Testcontainers + * + * @return a new WebDriver instance based on the browser set + */ + public abstract WebDriver createTestContainerDriver(); +} diff --git a/src/main/java/driver/DriverManager.java b/src/main/java/com/eliasnogueira/driver/DriverManager.java similarity index 88% rename from src/main/java/driver/DriverManager.java rename to src/main/java/com/eliasnogueira/driver/DriverManager.java index a4f614d..680076f 100644 --- a/src/main/java/driver/DriverManager.java +++ b/src/main/java/com/eliasnogueira/driver/DriverManager.java @@ -22,9 +22,8 @@ * SOFTWARE. */ -package driver; +package com.eliasnogueira.driver; -import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; @@ -48,10 +47,11 @@ public static void quit() { } public static String getInfo() { - Capabilities cap = ((RemoteWebDriver) DriverManager.getDriver()).getCapabilities(); + var cap = ((RemoteWebDriver) DriverManager.getDriver()).getCapabilities(); String browserName = cap.getBrowserName(); - String platform = cap.getPlatform().toString(); - String version = cap.getVersion(); + String platform = cap.getPlatformName().toString(); + String version = cap.getBrowserVersion(); + return String.format("browser: %s v: %s platform: %s", browserName, version, platform); } } diff --git a/src/main/java/com/eliasnogueira/driver/TargetFactory.java b/src/main/java/com/eliasnogueira/driver/TargetFactory.java new file mode 100644 index 0000000..3919b61 --- /dev/null +++ b/src/main/java/com/eliasnogueira/driver/TargetFactory.java @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2021 Elias Nogueira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.eliasnogueira.driver; + +import com.eliasnogueira.enums.Target; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.MutableCapabilities; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.RemoteWebDriver; + +import java.net.URI; + +import static com.eliasnogueira.config.ConfigurationManager.configuration; +import static com.eliasnogueira.driver.BrowserFactory.valueOf; +import static java.lang.String.format; + +public class TargetFactory { + + private static final Logger logger = LogManager.getLogger(TargetFactory.class); + + public WebDriver createInstance(String browser) { + Target target = Target.get(configuration().target().toUpperCase()); + + return switch (target) { + case LOCAL -> valueOf(configuration().browser().toUpperCase()).createLocalDriver(); + case LOCAL_SUITE -> valueOf(browser.toUpperCase()).createLocalDriver(); + case SELENIUM_GRID -> createRemoteInstance(valueOf(browser.toUpperCase()).getOptions()); + case TESTCONTAINERS -> valueOf(configuration().browser().toUpperCase()).createTestContainerDriver(); + }; + } + + private RemoteWebDriver createRemoteInstance(MutableCapabilities capability) { + RemoteWebDriver remoteWebDriver = null; + try { + String gridURL = format("http://%s:%s", configuration().gridUrl(), configuration().gridPort()); + + remoteWebDriver = new RemoteWebDriver(URI.create(gridURL).toURL(), capability); + } catch (java.net.MalformedURLException e) { + logger.error("Grid URL is invalid or Grid is not available"); + logger.error("Browser: {}", capability.getBrowserName(), e); + } catch (IllegalArgumentException e) { + logger.error("Browser {} is not valid or recognized", capability.getBrowserName(), e); + } + + return remoteWebDriver; + } +} diff --git a/src/main/java/enums/RoomType.java b/src/main/java/com/eliasnogueira/enums/RoomType.java similarity index 85% rename from src/main/java/enums/RoomType.java rename to src/main/java/com/eliasnogueira/enums/RoomType.java index 0b7fab8..d7b0980 100644 --- a/src/main/java/enums/RoomType.java +++ b/src/main/java/com/eliasnogueira/enums/RoomType.java @@ -22,11 +22,11 @@ * SOFTWARE. */ -package enums; +package com.eliasnogueira.enums; -import java.util.Random; +import java.util.function.Supplier; -public enum RoomType { +public enum RoomType implements Supplier { SINGLE("Single"), FAMILY("Family"), BUSINESS("Business"); @@ -36,12 +36,8 @@ public enum RoomType { this.value = value; } - public static RoomType getRandom() { - return values()[new Random().nextInt(values().length)]; - } - @Override - public String toString() { - return value; + public String get() { + return this.value; } -} \ No newline at end of file +} diff --git a/src/main/java/enums/Target.java b/src/main/java/com/eliasnogueira/enums/Target.java similarity index 51% rename from src/main/java/enums/Target.java rename to src/main/java/com/eliasnogueira/enums/Target.java index 21d0dfd..d9dfa4e 100644 --- a/src/main/java/enums/Target.java +++ b/src/main/java/com/eliasnogueira/enums/Target.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2018 Elias Nogueira + * Copyright (c) 2022 Elias Nogueira * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,37 @@ * SOFTWARE. */ -package enums; +package com.eliasnogueira.enums; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; public enum Target { - LOCAL, GRID + + LOCAL("local"), LOCAL_SUITE("local-suite"), SELENIUM_GRID("selenium-grid"), + TESTCONTAINERS("testcontainers"); + + private final String value; + private static final Map ENUM_MAP; + + Target(String value) { + this.value = value; + } + + static { + Map map = stream(Target.values()) + .collect(toMap(instance -> instance.value.toLowerCase(), instance -> instance, (_, b) -> b, ConcurrentHashMap::new)); + ENUM_MAP = Collections.unmodifiableMap(map); + } + + public static Target get(String value) { + if (!ENUM_MAP.containsKey(value.toLowerCase())) + throw new IllegalArgumentException(String.format("Value %s not valid. Use one of the TARGET enum values", value)); + + return ENUM_MAP.get(value.toLowerCase()); + } } diff --git a/src/main/java/driver/IDriver.java b/src/main/java/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java similarity index 78% rename from src/main/java/driver/IDriver.java rename to src/main/java/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java index 61ef761..67e6a0e 100644 --- a/src/main/java/driver/IDriver.java +++ b/src/main/java/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2018 Elias Nogueira + * Copyright (c) 2021 Elias Nogueira * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,11 +22,11 @@ * SOFTWARE. */ -package driver; +package com.eliasnogueira.exceptions; -import org.openqa.selenium.WebDriver; +public class HeadlessNotSupportedException extends IllegalStateException { -public interface IDriver { - - WebDriver createInstance(String browser); + public HeadlessNotSupportedException(String browser) { + super(String.format("Headless not supported for %s browser", browser)); + } } diff --git a/src/main/java/com/eliasnogueira/model/Booking.java b/src/main/java/com/eliasnogueira/model/Booking.java new file mode 100644 index 0000000..5348d03 --- /dev/null +++ b/src/main/java/com/eliasnogueira/model/Booking.java @@ -0,0 +1,81 @@ +/* + * MIT License + * + * Copyright (c) 2018 Elias Nogueira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.eliasnogueira.model; + +import com.eliasnogueira.enums.RoomType; + +public record Booking(String email, String country, String password, String dailyBudget, Boolean newsletter, + RoomType roomType, String roomDescription) { + + public static final class BookingBuilder { + + private String email; + private String country; + private String password; + private String dailyBudget; + private Boolean newsletter; + private RoomType roomType; + private String roomDescription; + + public BookingBuilder email(String email) { + this.email = email; + return this; + } + + public BookingBuilder country(String country) { + this.country = country; + return this; + } + + public BookingBuilder password(String password) { + this.password = password; + return this; + } + + public BookingBuilder dailyBudget(String dailyBudget) { + this.dailyBudget = dailyBudget; + return this; + } + + public BookingBuilder newsletter(Boolean newsletter) { + this.newsletter = newsletter; + return this; + } + + public BookingBuilder roomType(RoomType roomType) { + this.roomType = roomType; + return this; + } + + public BookingBuilder roomDescription(String roomDescription) { + this.roomDescription = roomDescription; + return this; + } + + public Booking build() { + return new Booking(email, country, password, dailyBudget, newsletter, roomType, roomDescription); + } + } +} diff --git a/src/main/java/page/objects/AbstractPageObject.java b/src/main/java/com/eliasnogueira/page/AbstractPageObject.java similarity index 75% rename from src/main/java/page/objects/AbstractPageObject.java rename to src/main/java/com/eliasnogueira/page/AbstractPageObject.java index 3282069..827215d 100644 --- a/src/main/java/page/objects/AbstractPageObject.java +++ b/src/main/java/com/eliasnogueira/page/AbstractPageObject.java @@ -22,20 +22,17 @@ * SOFTWARE. */ -package page.objects; +package com.eliasnogueira.page; -import config.Configuration; -import config.ConfigurationManager; -import driver.DriverManager; -import org.openqa.selenium.support.PageFactory; +import com.eliasnogueira.driver.DriverManager; import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory; +import static com.eliasnogueira.config.ConfigurationManager.configuration; +import static org.openqa.selenium.support.PageFactory.initElements; + public class AbstractPageObject { protected AbstractPageObject() { - Configuration configuration = ConfigurationManager.getConfiguration(); - int timeout = Integer.parseInt(configuration.timeout()); - - PageFactory.initElements(new AjaxElementLocatorFactory(DriverManager.getDriver(), timeout), this); + initElements(new AjaxElementLocatorFactory(DriverManager.getDriver(), configuration().timeout()), this); } } diff --git a/src/main/java/page/objects/booking/AccountPage.java b/src/main/java/com/eliasnogueira/page/booking/AccountPage.java similarity index 92% rename from src/main/java/page/objects/booking/AccountPage.java rename to src/main/java/com/eliasnogueira/page/booking/AccountPage.java index 8323433..aae5331 100644 --- a/src/main/java/page/objects/booking/AccountPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/AccountPage.java @@ -22,12 +22,13 @@ * SOFTWARE. */ -package page.objects.booking; +package com.eliasnogueira.page.booking; +import com.eliasnogueira.page.booking.common.NavigationPage; +import io.qameta.allure.Step; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.ui.Select; -import page.objects.booking.common.NavigationPage; public class AccountPage extends NavigationPage { @@ -46,22 +47,27 @@ public class AccountPage extends NavigationPage { @FindBy(css = ".check") private WebElement newsletter; + @Step public void fillEmail(String email) { this.email.sendKeys(email); } + @Step public void fillPassword(String password) { this.password.sendKeys(password); } + @Step public void selectCountry(String country) { new Select(this.country).selectByVisibleText(country); } + @Step public void selectBudget(String value) { new Select(budget).selectByVisibleText(value); } + @Step public void clickNewsletter() { newsletter.click(); } diff --git a/src/main/java/page/objects/booking/DetailPage.java b/src/main/java/com/eliasnogueira/page/booking/DetailPage.java similarity index 89% rename from src/main/java/page/objects/booking/DetailPage.java rename to src/main/java/com/eliasnogueira/page/booking/DetailPage.java index c7181e1..2ba247f 100644 --- a/src/main/java/page/objects/booking/DetailPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/DetailPage.java @@ -22,13 +22,14 @@ * SOFTWARE. */ -package page.objects.booking; +package com.eliasnogueira.page.booking; -import driver.DriverManager; +import com.eliasnogueira.driver.DriverManager; +import com.eliasnogueira.page.booking.common.NavigationPage; +import io.qameta.allure.Step; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.FindBy; -import page.objects.booking.common.NavigationPage; public class DetailPage extends NavigationPage { @@ -38,10 +39,12 @@ public class DetailPage extends NavigationPage { @FindBy(css = "#message > p") private WebElement message; + @Step public void fillRoomDescription(String description) { new Actions(DriverManager.getDriver()).sendKeys(roomDescription, description); } + @Step public String getAlertMessage() { return message.getText(); } diff --git a/src/main/java/page/objects/booking/RoomPage.java b/src/main/java/com/eliasnogueira/page/booking/RoomPage.java similarity index 84% rename from src/main/java/page/objects/booking/RoomPage.java rename to src/main/java/com/eliasnogueira/page/booking/RoomPage.java index 67a3327..5b14b06 100644 --- a/src/main/java/page/objects/booking/RoomPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/RoomPage.java @@ -22,16 +22,17 @@ * SOFTWARE. */ -package page.objects.booking; +package com.eliasnogueira.page.booking; -import driver.DriverManager; -import enums.RoomType; +import com.eliasnogueira.driver.DriverManager; +import com.eliasnogueira.page.booking.common.NavigationPage; +import io.qameta.allure.Step; import org.openqa.selenium.By; -import page.objects.booking.common.NavigationPage; public class RoomPage extends NavigationPage { - public void selectRoomType(RoomType room) { + @Step + public void selectRoomType(String room) { DriverManager.getDriver().findElement(By.xpath("//h6[text()='" + room + "']")).click(); } } diff --git a/src/main/java/page/objects/booking/common/NavigationPage.java b/src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java similarity index 91% rename from src/main/java/page/objects/booking/common/NavigationPage.java rename to src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java index 12dedde..ceafb1d 100644 --- a/src/main/java/page/objects/booking/common/NavigationPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java @@ -22,11 +22,12 @@ * SOFTWARE. */ -package page.objects.booking.common; +package com.eliasnogueira.page.booking.common; +import com.eliasnogueira.page.AbstractPageObject; +import io.qameta.allure.Step; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; -import page.objects.AbstractPageObject; public class NavigationPage extends AbstractPageObject { @@ -39,14 +40,12 @@ public class NavigationPage extends AbstractPageObject { @FindBy(name = "finish") private WebElement finish; + @Step public void next() { next.click(); } - public void previous() { - previous.click(); - } - + @Step public void finish() { finish.click(); } diff --git a/src/main/java/com/eliasnogueira/report/AllureManager.java b/src/main/java/com/eliasnogueira/report/AllureManager.java new file mode 100644 index 0000000..2180e2e --- /dev/null +++ b/src/main/java/com/eliasnogueira/report/AllureManager.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2018 Elias Nogueira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.eliasnogueira.report; + +import com.eliasnogueira.enums.Target; +import com.github.automatedowl.tools.AllureEnvironmentWriter; +import com.google.common.collect.ImmutableMap; + +import java.util.HashMap; +import java.util.Map; + +import static com.eliasnogueira.config.ConfigurationManager.configuration; + +public class AllureManager { + + private AllureManager() { + } + + public static void setAllureEnvironmentInformation() { + var basicInfo = new HashMap<>(Map.of( + "Test URL", configuration().url(), + "Target execution", configuration().target(), + "Global timeout", String.valueOf(configuration().timeout()), + "Headless mode", String.valueOf(configuration().headless()), + "Faker locale", configuration().faker(), + "Local browser", configuration().browser() + )); + + if (configuration().target().equals(Target.SELENIUM_GRID.name())) { + var gridMap = Map.of("Grid URL", configuration().gridUrl(), "Grid port", configuration().gridPort()); + basicInfo.putAll(gridMap); + } + + AllureEnvironmentWriter.allureEnvironmentWriter(ImmutableMap.copyOf(basicInfo)); + } +} diff --git a/src/main/java/driver/DriverFactory.java b/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java similarity index 50% rename from src/main/java/driver/DriverFactory.java rename to src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java index 8fc178e..4d09ef2 100644 --- a/src/main/java/driver/DriverFactory.java +++ b/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2018 Elias Nogueira + * Copyright (c) 2024 Elias Nogueira * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,41 +21,36 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package driver; - -import config.Configuration; -import config.ConfigurationManager; -import driver.local.LocalDriverManager; -import driver.remote.RemoteDriverManager; -import enums.Target; -import lombok.extern.log4j.Log4j2; +package com.eliasnogueira.report; + +import com.eliasnogueira.driver.DriverManager; +import io.qameta.allure.Attachment; +import io.qameta.allure.listener.TestLifecycleListener; +import io.qameta.allure.model.TestResult; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; -@Log4j2 -public class DriverFactory { +import static io.qameta.allure.model.Status.BROKEN; +import static io.qameta.allure.model.Status.FAILED; - private DriverFactory() {} +/* + * Approach implemented using the https://github.com/biczomate/allure-testng7.5-attachment-example as reference + */ +public class AllureTestLifecycleListener implements TestLifecycleListener { - public static WebDriver createInstance(String browser) { - Configuration configuration = ConfigurationManager.getConfiguration(); - Target target = Target.valueOf(configuration.target().toUpperCase()); - WebDriver webdriver; + public AllureTestLifecycleListener() { + } - switch (target) { + @Attachment(value = "Page Screenshot", type = "image/png") + public byte[] saveScreenshot(WebDriver driver) { + return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + } - case LOCAL: - //override the browser value from @Optional on BeseWeb - webdriver = new LocalDriverManager().createInstance(configuration.browser()); - break; - case GRID: - // getting the browser from the suite file or @Optional on BaseWeb - webdriver = new RemoteDriverManager().createInstance(browser); - break; - default: - throw new IllegalStateException("Unexpected value: " + target); + @Override + public void beforeTestStop(TestResult result) { + if (FAILED == result.getStatus() || BROKEN == result.getStatus()) { + saveScreenshot(DriverManager.getDriver()); } - - return webdriver; } } diff --git a/src/main/java/driver/local/LocalDriverManager.java b/src/main/java/driver/local/LocalDriverManager.java deleted file mode 100644 index 8b9d82e..0000000 --- a/src/main/java/driver/local/LocalDriverManager.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018 Elias Nogueira - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package driver.local; - -import static java.lang.Boolean.TRUE; - -import config.Configuration; -import config.ConfigurationManager; -import driver.IDriver; -import io.github.bonigarcia.wdm.WebDriverManager; -import io.github.bonigarcia.wdm.config.DriverManagerType; -import java.lang.reflect.InvocationTargetException; -import lombok.extern.log4j.Log4j2; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.firefox.FirefoxDriver; -import org.openqa.selenium.firefox.FirefoxOptions; - -@Log4j2 -public class LocalDriverManager implements IDriver { - - @Override - public WebDriver createInstance(String browser) { - WebDriver driverInstance = null; - - try { - DriverManagerType driverManagerType = DriverManagerType.valueOf(browser.toUpperCase()); - Class driverClass = Class.forName(driverManagerType.browserClass()); - WebDriverManager.getInstance(driverManagerType).setup(); - Configuration configuration = ConfigurationManager.getConfiguration(); - - if (TRUE.equals(configuration.headless())) { - driverInstance = defineHeadless(driverClass); - } else { - driverInstance = (WebDriver) driverClass.getDeclaredConstructor().newInstance(); - } - - } catch (IllegalAccessException | ClassNotFoundException e) { - log.error("The class could not be found", e); - } catch (InstantiationException | NoSuchMethodException | InvocationTargetException e) { - log.error("Problem during driver instantiation", e); - } - return driverInstance; - } - - private WebDriver defineHeadless(Class driverClass) { - WebDriver driver; - - if (ChromeDriver.class == driverClass) { - driver = new ChromeDriver(new ChromeOptions().setHeadless(true)); - } else if (FirefoxDriver.class == driverClass) { - driver = new FirefoxDriver(new FirefoxOptions().setHeadless(true)); - } else { - throw new IllegalArgumentException("Headless is only supported by Google Chrome or Firefox"); - } - - return driver; - } -} diff --git a/src/main/java/driver/remote/RemoteDriverManager.java b/src/main/java/driver/remote/RemoteDriverManager.java deleted file mode 100644 index aff023d..0000000 --- a/src/main/java/driver/remote/RemoteDriverManager.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018 Elias Nogueira - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package driver.remote; - -import config.Configuration; -import config.ConfigurationManager; -import driver.IDriver; -import io.github.bonigarcia.wdm.config.DriverManagerType; -import java.net.MalformedURLException; -import java.net.URL; -import lombok.extern.log4j.Log4j2; -import org.openqa.selenium.MutableCapabilities; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.edge.EdgeOptions; -import org.openqa.selenium.firefox.FirefoxOptions; -import org.openqa.selenium.ie.InternetExplorerOptions; -import org.openqa.selenium.opera.OperaOptions; -import org.openqa.selenium.remote.RemoteWebDriver; - -@Log4j2 -public class RemoteDriverManager implements IDriver { - - @Override - public WebDriver createInstance(String browser) { - RemoteWebDriver remoteWebDriver = null; - Configuration configuration = ConfigurationManager.getConfiguration(); - try { - // a composition of the target grid address and port - String gridURL = String.format("http://%s:%s/wd/hub", configuration.gridUrl(), configuration.gridPort()); - - remoteWebDriver = new RemoteWebDriver(new URL(gridURL), getCapability(browser)); - } catch (MalformedURLException e) { - log.error("Grid URL is invalid or Grid is not available"); - log.error("Browser: " + browser, e); - } catch (IllegalArgumentException e) { - log.error("Browser: " + browser + "is not valid or recognized", e); - } - - return remoteWebDriver; - } - - private static MutableCapabilities getCapability(String browser) { - MutableCapabilities mutableCapabilities; - DriverManagerType driverManagerType = DriverManagerType.valueOf(browser.toUpperCase()); - - switch (driverManagerType) { - - case CHROME: - mutableCapabilities = defaultChromeOptions(); - break; - case FIREFOX: - mutableCapabilities = new FirefoxOptions(); - break; - case OPERA: - mutableCapabilities = new OperaOptions(); - break; - case EDGE: - mutableCapabilities = new EdgeOptions(); - break; - case IEXPLORER: - mutableCapabilities = new InternetExplorerOptions(); - break; - case PHANTOMJS: - case SELENIUM_SERVER_STANDALONE: - throw new IllegalStateException("Not supported: " + driverManagerType); - default: - throw new IllegalStateException("Unexpected value: " + driverManagerType); - } - - return mutableCapabilities; - } - - private static MutableCapabilities defaultChromeOptions() { - ChromeOptions capabilities = new ChromeOptions(); - capabilities.addArguments("start-maximized"); - - return capabilities; - } -} diff --git a/src/main/java/report/AllureManager.java b/src/main/java/report/AllureManager.java deleted file mode 100644 index ac9212b..0000000 --- a/src/main/java/report/AllureManager.java +++ /dev/null @@ -1,36 +0,0 @@ -package report; - -import com.github.automatedowl.tools.AllureEnvironmentWriter; -import com.google.common.collect.ImmutableMap; -import config.Configuration; -import config.ConfigurationManager; -import driver.DriverManager; -import io.qameta.allure.Attachment; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; - -public class AllureManager { - - private AllureManager() {} - - public static void setAllureEnvironmentInformation() { - Configuration configuration = ConfigurationManager.getConfiguration(); - - AllureEnvironmentWriter.allureEnvironmentWriter( - ImmutableMap.builder(). - put("URL", configuration.url()). - put("Target", configuration.target()). - build()); - } - - @Attachment(value = "Failed test screenshot", type = "image/png") - public static byte[] takeScreenshotToAttachOnAllureReport() { - return ((TakesScreenshot) DriverManager.getDriver()).getScreenshotAs(OutputType.BYTES); - } - - @Attachment(value = "Browser information", type = "text/plain") - public static String addBrowserInformationOnAllureReport() { - return DriverManager.getInfo(); - } - -} diff --git a/src/main/java/test/TestListener.java b/src/main/java/test/TestListener.java deleted file mode 100644 index 1a4091a..0000000 --- a/src/main/java/test/TestListener.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018 Elias Nogueira - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package test; - -import lombok.extern.log4j.Log4j2; -import org.testng.ITestContext; -import org.testng.ITestListener; -import org.testng.ITestResult; -import report.AllureManager; - -@Log4j2 -public class TestListener implements ITestListener { - - @Override - public void onTestStart(ITestResult result) { - // empty - } - - @Override - public void onTestSuccess(ITestResult result) { - // empty - } - - @Override - public void onTestFailure(ITestResult result) { - failTest(result); - } - - @Override - public void onTestSkipped(ITestResult result) { - log.error(result.getThrowable()); - } - - @Override - public void onTestFailedButWithinSuccessPercentage(ITestResult result) { - // empty - } - - @Override - public void onStart(ITestContext context) { - // empty - } - - @Override - public void onFinish(ITestContext context) { - // empty - } - - private void failTest(ITestResult iTestResult) { - log.error(iTestResult.getTestClass().getName()); - log.error(iTestResult.getThrowable()); - - AllureManager.takeScreenshotToAttachOnAllureReport(); - } -} diff --git a/src/main/resources/conf/general.properties b/src/main/resources/conf/general.properties deleted file mode 100644 index 45685b5..0000000 --- a/src/main/resources/conf/general.properties +++ /dev/null @@ -1,14 +0,0 @@ -# target execution: local or grid -target = local - -# initial URL -url.base = https://page-example.imfast.io/ - -# global test timeout -timeout = 3 - -# javafaker locale -faker.locale = pt-BR - -# headless mode only for chrome or firefox and local execution -headless = true \ No newline at end of file diff --git a/src/main/resources/conf/local.properties b/src/main/resources/conf/local.properties deleted file mode 100644 index 07d1868..0000000 --- a/src/main/resources/conf/local.properties +++ /dev/null @@ -1 +0,0 @@ -browser = chrome \ No newline at end of file diff --git a/src/main/resources/log4j2.properties b/src/main/resources/log4j2.properties index 953edb4..d41ecde 100644 --- a/src/main/resources/log4j2.properties +++ b/src/main/resources/log4j2.properties @@ -13,7 +13,7 @@ appender.file.fileName=${sys:user.home}/test_automation.log appender.file.layout.type=PatternLayout appender.file.layout.pattern=[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n -rootLogger.level = debug +rootLogger.level = info rootLogger.appenderRefs = stdout rootLogger.appenderRef.stdout.ref = STDOUT -rootLogger.appenderRef.file.ref = LOGFILE \ No newline at end of file +rootLogger.appenderRef.file.ref = LOGFILE diff --git a/src/main/java/test/BaseWeb.java b/src/test/java/com/eliasnogueira/BaseWeb.java similarity index 80% rename from src/main/java/test/BaseWeb.java rename to src/test/java/com/eliasnogueira/BaseWeb.java index 7fcd600..cd5c894 100644 --- a/src/main/java/test/BaseWeb.java +++ b/src/test/java/com/eliasnogueira/BaseWeb.java @@ -22,22 +22,20 @@ * SOFTWARE. */ -package test; +package com.eliasnogueira; -import config.Configuration; -import config.ConfigurationManager; -import driver.DriverFactory; -import driver.DriverManager; +import com.eliasnogueira.driver.DriverManager; +import com.eliasnogueira.driver.TargetFactory; +import com.eliasnogueira.report.AllureManager; import org.openqa.selenium.WebDriver; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeSuite; -import org.testng.annotations.Listeners; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; -import report.AllureManager; -@Listeners({TestListener.class}) +import static com.eliasnogueira.config.ConfigurationManager.configuration; + public abstract class BaseWeb { @BeforeSuite @@ -48,12 +46,10 @@ public void beforeSuite() { @BeforeMethod(alwaysRun = true) @Parameters("browser") public void preCondition(@Optional("chrome") String browser) { - Configuration configuration = ConfigurationManager.getConfiguration(); - - WebDriver driver = DriverFactory.createInstance(browser); + WebDriver driver = new TargetFactory().createInstance(browser); DriverManager.setDriver(driver); - DriverManager.getDriver().get(configuration.url()); + DriverManager.getDriver().get(configuration().url()); } @AfterMethod(alwaysRun = true) diff --git a/src/test/java/test/BookRoomWebTest.java b/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java similarity index 61% rename from src/test/java/test/BookRoomWebTest.java rename to src/test/java/com/eliasnogueira/test/BookRoomWebTest.java index ae404a7..7471352 100644 --- a/src/test/java/test/BookRoomWebTest.java +++ b/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java @@ -21,40 +21,40 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package test; +package com.eliasnogueira.test; -import static org.assertj.core.api.Assertions.assertThat; - -import data.BookingDataFactory; -import model.Booking; +import com.eliasnogueira.BaseWeb; +import com.eliasnogueira.data.dynamic.BookingDataFactory; +import com.eliasnogueira.page.booking.AccountPage; +import com.eliasnogueira.page.booking.DetailPage; +import com.eliasnogueira.page.booking.RoomPage; import org.testng.annotations.Test; -import page.objects.booking.AccountPage; -import page.objects.booking.DetailPage; -import page.objects.booking.RoomPage; + +import static org.assertj.core.api.Assertions.assertThat; public class BookRoomWebTest extends BaseWeb { @Test(description = "Book a room") public void bookARoom() { - Booking bookingInformation = new BookingDataFactory().createBookingData(); + var bookingInformation = BookingDataFactory.createBookingData(); - AccountPage accountPage = new AccountPage(); - accountPage.fillEmail(bookingInformation.getEmail()); - accountPage.fillPassword(bookingInformation.getPassword()); - accountPage.selectCountry(bookingInformation.getCountry()); - accountPage.selectBudget(bookingInformation.getDailyBudget()); + var accountPage = new AccountPage(); + accountPage.fillEmail(bookingInformation.email()); + accountPage.fillPassword(bookingInformation.password()); + accountPage.selectCountry(bookingInformation.country()); + accountPage.selectBudget(bookingInformation.dailyBudget()); accountPage.clickNewsletter(); accountPage.next(); - RoomPage roomPage = new RoomPage(); - roomPage.selectRoomType(bookingInformation.getRoomType()); + var roomPage = new RoomPage(); + roomPage.selectRoomType(bookingInformation.roomType().get()); roomPage.next(); - DetailPage detailPage = new DetailPage(); - detailPage.fillRoomDescription(bookingInformation.getRoomDescription()); + var detailPage = new DetailPage(); + detailPage.fillRoomDescription(bookingInformation.roomDescription()); detailPage.finish(); assertThat(detailPage.getAlertMessage()) - .isEqualTo("Your reservation has been made and we will contact you shortly"); + .isEqualTo("Your reservation has been made and we will contact you shortly"); } } diff --git a/src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener b/src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener new file mode 100644 index 0000000..dda40f5 --- /dev/null +++ b/src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener @@ -0,0 +1 @@ +com.eliasnogueira.report.AllureTestLifecycleListener diff --git a/src/test/resources/allure.properties b/src/test/resources/allure.properties new file mode 100644 index 0000000..80b02dd --- /dev/null +++ b/src/test/resources/allure.properties @@ -0,0 +1 @@ +allure.results.directory=target/allure-results diff --git a/src/test/resources/general.properties b/src/test/resources/general.properties new file mode 100644 index 0000000..15bb713 --- /dev/null +++ b/src/test/resources/general.properties @@ -0,0 +1,17 @@ +# target execution: local, selenium-grid or testcontainers +target = local + +# browser to use for local and testcontainers execution +browser = chrome + +# initial URL +url.base = https://eliasnogueira.com/external/selenium-java-architecture/ + +# global test timeout +timeout = 3 + +# datafaker locale +faker.locale = en-US + +# headless mode only for chrome or firefox and local execution +headless = false diff --git a/src/main/resources/conf/grid.properties b/src/test/resources/selenium-grid.properties similarity index 70% rename from src/main/resources/conf/grid.properties rename to src/test/resources/selenium-grid.properties index eb20844..90d2540 100644 --- a/src/main/resources/conf/grid.properties +++ b/src/test/resources/selenium-grid.properties @@ -1,3 +1,3 @@ # grid url and port grid.url = localhost -grid.port = 4444 \ No newline at end of file +grid.port = 4444 diff --git a/src/test/resources/suites/local.xml b/src/test/resources/suites/local.xml new file mode 100644 index 0000000..089ee73 --- /dev/null +++ b/src/test/resources/suites/local.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/test/resources/suites/multi_browser.xml b/src/test/resources/suites/selenium-grid.xml similarity index 74% rename from src/test/resources/suites/multi_browser.xml rename to src/test/resources/suites/selenium-grid.xml index 9bec0be..04d9024 100644 --- a/src/test/resources/suites/multi_browser.xml +++ b/src/test/resources/suites/selenium-grid.xml @@ -4,15 +4,15 @@ - + - + - \ No newline at end of file + diff --git a/src/test/resources/suites/suite.xml b/src/test/resources/suites/suite.xml deleted file mode 100644 index b5bfca7..0000000 --- a/src/test/resources/suites/suite.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file