diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9f39b7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Use this to log an issue/bug/improvement +title: '' +labels: '' +assignees: '' + +--- + +**Describe the issue** +A clear and concise description of the problem + +**Steps** +Steps to reproduce the unexpected behaviour: +1. Go to '...' +2. Click on '....' +3. See the error + +**Expected behaviour** +A clear and concise description of what you expected to happen. + +**Additional information** +Any other relevant information diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8043827 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: maven + directory: "/" + schedule: + interval: daily + time: '04:00' + open-pull-requests-limit: 10 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ee2c216 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +**IMPORTANT: Please do not create a Pull Request without creating an issue first.** + +*Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.* + +Please provide enough information so that others can review your pull request: + + + +Explain the **details** for making this change. What existing problem does the pull request solve? + + + +**Test plan (required)** + +Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. + + + +**Code formatting** + + + +**Closing issues** + +Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). 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/.gitignore b/.gitignore index 5381bfd..6cf5705 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .settings/ +grid/assets # Intellij .idea/ @@ -6,4 +7,7 @@ # Maven logs/ -target/ \ No newline at end of file +target/ + +# Allure +.allure \ No newline at end of file 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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..879dea0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at elias.nogueira@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..21a0ef8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# 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-lean-test-achitecture/issues). + +Select _Bug report_ if tou want log an issue or _Feature request_ if you want to see something new. + +Do not forget to add a _label_ on the issue or feature. + +## I have enough knowledge in development to colaborate +Excellent! Thank you to help me out! + +You're going to need a few things first: +* JDK 17+ +* [Configure your IDE](https://projectlombok.org/setup/overview) in order to support Lombok. + +## Send a pull request +Please add an explanation about the pull request. +If there is or you created an issue/feature, please add the link. + +Pull requests without explanations will be rejected, so please add: +* the reason about you're sending the pull request +* the benefit that your pull request will bring to the application diff --git a/README.MD b/README.MD index 8c9160e..d90fecc 100644 --- a/README.MD +++ b/README.MD @@ -1,115 +1,352 @@ -The purpose of this project is to have a basic project with minimum viable testing architecture to automated web tests using Selenium WebDriver and Java as the programming language. +# Lean Test Automation Architecture using Java and Selenium WebDriver + +[![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 8 as the programming language -* TestNG as the UnitTest framework to support the test creation +* [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 +* [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 +* [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 +* [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: -* Use of Page Objects patters -* Parallel execution -* BaseTest -* TestListner -* Logging -* Configuration through a properties file -* yaml templates to create a Selenium Grid infrastructure on OpenShift +* [Page Objects pattern](#page-objects-pattern) +* [Execution types](#execution-types) +* [BaseTest](#basetest) +* [TestListener](#testlistener) +* [Logging](#logging) +* [Configuration files](#configuration-files) +* [Parallel execution](#parallel-execution) +* [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. -### Use of Page Objects patters +### Page Objects pattern -### Parallel execution +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. +Also, it sets the timeout from the `timeout` property value located on `general.properties` file. + +All the Page Object classes should extend the `AbstractPageObject`. +It also tries to remove the `driver` object from the Page Object class as much as possible. + +> **Important information** +> +> 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 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 different execution types: + +- `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 + +##### 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 + +##### 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. + +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 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 + +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 -### TestListner +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 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 in the test class. + +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 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 -### Configuration through a properties file +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. +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-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) -The project use the a property file to configure basic mutable items, like: +### Parallel execution -* base.url: the main app URL -* grid.url: target grid url -* grid.port: target grid port -* log.directory = the name of the log folder -* log.dateformat = the data format for the log filename +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: -The property file is inside _conf_ folder. This folder has three sub-folders: +```xml -* dev: -* test: -* prod: + +``` -You can set different values for the properties keys based on your environment. -When you run a test without inform the parameter `env`, the property file used will be _conf/dev/config.properties_. +You can define any parallel strategy. -To change the environment, just use `-Denv=env_name` where _env_name_ is the name of the folder inside _conf_ folder. +It can be an excellent combination together with the grid strategy. -E.g: running all tests using test configuration values +#### Execution with Docker Selenium Distributed -``` bash -mvn test -Denv=test +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 +file reader in the following classes: + +* [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/`: + +* `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-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-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. - -#### Jenkins pipeline +The two files of the pipeline as a code are inside `pipeline_as_code` folder. -You can you _Jenkisfile_ on this directory and refer _pipeline_as_code/Jenkinsfile_ on XXXXXX item on Jenkins. -This pipeline works on +* 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/assets/example_filed_test_with_report.gif b/assets/example_filed_test_with_report.gif new file mode 100644 index 0000000..3afab95 Binary files /dev/null and b/assets/example_filed_test_with_report.gif differ 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 f988931..d3faf76 100644 --- a/pom.xml +++ b/pom.xml @@ -5,35 +5,41 @@ 4.0.0 com.eliasnogueira - selenium-java-bootstrap - 1.1 + selenium-java-lean-test-architecture + 3.8.0 - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 8 - 8 - - - - + + scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git + scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git + + - 3.141.59 - 7.1.0 - 3.14.0 - 4.0.9 - 1.0.3 - 1.0.1 - 2.13.0 - 1.18.10 - 3.7.1 - - multi_browser + 24 + UTF-8 + UTF-8 + 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.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 + + local @@ -43,12 +49,6 @@ ${selenium.version} - - org.seleniumhq.selenium - selenium-server - ${selenium.version} - - org.testng testng @@ -62,57 +62,81 @@ - com.aventstack - extentreports - ${extentreports.version} + net.datafaker + datafaker + ${datafaker.version} - com.github.javafaker - javafaker - ${javafaker.version} + org.apache.logging.log4j + log4j-api + ${log4j.version} org.apache.logging.log4j - log4j-api + log4j-core ${log4j.version} org.apache.logging.log4j - log4j-core + log4j-slf4j-impl ${log4j.version} - org.projectlombok - lombok - ${lombok.version} - provided + org.aeonbits.owner + owner + ${owner.version} - com.aventstack - extentreports-testng-adapter - ${extentreports-adapter.version} + io.qameta.allure + allure-testng + ${allure-testng.version} - org.aeonbits.owner - owner - 1.0.10 + io.qameta.allure + allure-attachments + ${allure-attachments.version} - io.github.bonigarcia - webdrivermanager - ${webdrivermanager.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 @@ -121,7 +145,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.12.4 + ${maven-surefire-plugin.version} src/test/resources/suites/${suite}.xml @@ -131,6 +155,51 @@ + - \ No newline at end of file + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + false + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + 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} + + + + + + + diff --git a/src/main/java/com/eliasnogueira/config/Configuration.java b/src/main/java/com/eliasnogueira/config/Configuration.java new file mode 100644 index 0000000..dda4233 --- /dev/null +++ b/src/main/java/com/eliasnogueira/config/Configuration.java @@ -0,0 +1,61 @@ +/* + * 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.config; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.LoadType; + +@LoadPolicy(LoadType.MERGE) +@Config.Sources({ + "system:properties", + "classpath:general.properties", + "classpath:selenium-grid.properties"}) +public interface Configuration extends Config { + + @Key("target") + String target(); + + @Key("browser") + String browser(); + + @Key("headless") + Boolean headless(); + + @Key("url.base") + String url(); + + @Key("timeout") + int timeout(); + + @Key("grid.url") + String gridUrl(); + + @Key("grid.port") + String gridPort(); + + @Key("faker.locale") + String faker(); +} diff --git a/src/main/java/driver/DriverFactory.java b/src/main/java/com/eliasnogueira/config/ConfigurationManager.java similarity index 56% rename from src/main/java/driver/DriverFactory.java rename to src/main/java/com/eliasnogueira/config/ConfigurationManager.java index 6b3d661..ce15843 100644 --- a/src/main/java/driver/DriverFactory.java +++ b/src/main/java/com/eliasnogueira/config/ConfigurationManager.java @@ -22,40 +22,16 @@ * SOFTWARE. */ -package driver; +package com.eliasnogueira.config; -import config.Configuration; -import driver.local.LocalDriver; -import driver.remote.RemoteDriver; -import lombok.extern.log4j.Log4j2; import org.aeonbits.owner.ConfigCache; -import org.openqa.selenium.WebDriver; -@Log4j2 -public class DriverFactory { +public class ConfigurationManager { - - public static WebDriver createInstance(String browser) { - Configuration configuration = ConfigCache.getOrCreate(Configuration.class); - Target target = Target.valueOf(configuration.target().toUpperCase()); - WebDriver webdriver; - - switch (target) { - - case LOCAL: - webdriver = new LocalDriver().createInstance(browser); - break; - case GRID: - webdriver = new RemoteDriver().createInstance(browser); - break; - default: - throw new IllegalStateException("Unexpected value: " + target); - } - - return webdriver; + private ConfigurationManager() { } - enum Target { - LOCAL, GRID + public static Configuration configuration() { + return ConfigCache.getOrCreate(Configuration.class); } } diff --git a/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java b/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java new file mode 100644 index 0000000..810154d --- /dev/null +++ b/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * 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 + * 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.data.changeless; + +public final class BrowserData { + + private BrowserData() { + } + + 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/com/eliasnogueira/data/dynamic/BookingDataFactory.java b/src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java new file mode 100644 index 0000000..c1dc321 --- /dev/null +++ b/src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java @@ -0,0 +1,67 @@ +/* + * 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.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 java.util.Locale; + +import static com.eliasnogueira.config.ConfigurationManager.configuration; + +public final class BookingDataFactory { + + private static final Faker faker = new Faker(new Locale.Builder().setLanguageTag(configuration().faker()).build()); + private static final Logger logger = LogManager.getLogger(BookingDataFactory.class); + + private BookingDataFactory() { + } + + 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(faker.options().option(RoomType.class)). + roomDescription(faker.lorem().paragraph()). + build(); + + logger.info(booking); + return booking; + } + + private static String returnRandomCountry() { + return faker.options().option("Belgium", "Brazil", "Netherlands"); + } + + 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 84% rename from src/main/java/driver/DriverManager.java rename to src/main/java/com/eliasnogueira/driver/DriverManager.java index cc10ae6..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; @@ -32,10 +31,10 @@ public class DriverManager { private static final ThreadLocal driver = new ThreadLocal<>(); - private DriverManager() {} + private DriverManager() {} public static WebDriver getDriver() { - return driver.get(); + return driver.get(); } public static void setDriver(WebDriver driver) { @@ -44,13 +43,15 @@ public static void setDriver(WebDriver driver) { public static void quit() { DriverManager.driver.get().quit(); + driver.remove(); } 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/com/eliasnogueira/enums/Target.java b/src/main/java/com/eliasnogueira/enums/Target.java new file mode 100644 index 0000000..d9dfa4e --- /dev/null +++ b/src/main/java/com/eliasnogueira/enums/Target.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * 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 + * 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.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("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/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java b/src/main/java/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java new file mode 100644 index 0000000..67e6a0e --- /dev/null +++ b/src/main/java/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java @@ -0,0 +1,32 @@ +/* + * 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.exceptions; + +public class HeadlessNotSupportedException extends IllegalStateException { + + 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 6338d65..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 driver.DriverManager; -import org.aeonbits.owner.ConfigCache; -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 = ConfigCache.getOrCreate(Configuration.class); - 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 91% rename from src/main/java/page/objects/booking/AccountPage.java rename to src/main/java/com/eliasnogueira/page/booking/AccountPage.java index a540cb4..aae5331 100644 --- a/src/main/java/page/objects/booking/AccountPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/AccountPage.java @@ -22,13 +22,13 @@ * SOFTWARE. */ -package page.objects.booking; +package com.eliasnogueira.page.booking; -import driver.DriverManager; +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 { @@ -47,26 +47,27 @@ public class AccountPage extends NavigationPage { @FindBy(css = ".check") private WebElement newsletter; - public AccountPage() { - DriverManager.getDriver().switchTo().frame("result"); - } - + @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 85% rename from src/main/java/page/objects/booking/DetailPage.java rename to src/main/java/com/eliasnogueira/page/booking/DetailPage.java index 8831451..2ba247f 100644 --- a/src/main/java/page/objects/booking/DetailPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/DetailPage.java @@ -22,16 +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 org.openqa.selenium.support.ui.ExpectedCondition; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; -import page.objects.booking.common.NavigationPage; public class DetailPage extends NavigationPage { @@ -41,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/com/eliasnogueira/report/AllureTestLifecycleListener.java b/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java new file mode 100644 index 0000000..4d09ef2 --- /dev/null +++ b/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java @@ -0,0 +1,56 @@ +/* + * MIT License + * + * 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 + * 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.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; + +import static io.qameta.allure.model.Status.BROKEN; +import static io.qameta.allure.model.Status.FAILED; + +/* + * Approach implemented using the https://github.com/biczomate/allure-testng7.5-attachment-example as reference + */ +public class AllureTestLifecycleListener implements TestLifecycleListener { + + public AllureTestLifecycleListener() { + } + + @Attachment(value = "Page Screenshot", type = "image/png") + public byte[] saveScreenshot(WebDriver driver) { + return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + } + + @Override + public void beforeTestStop(TestResult result) { + if (FAILED == result.getStatus() || BROKEN == result.getStatus()) { + saveScreenshot(DriverManager.getDriver()); + } + } +} diff --git a/src/main/java/config/Configuration.java b/src/main/java/config/Configuration.java deleted file mode 100644 index bf627cd..0000000 --- a/src/main/java/config/Configuration.java +++ /dev/null @@ -1,23 +0,0 @@ -package config; - -import org.aeonbits.owner.Config; - -@Config.Sources({"classpath:conf/${env}.properties"}) -public interface Configuration extends Config { - - String target(); - - @Key("url.base") - String url(); - - String timeout(); - - @Key("grid.url") - String gridUrl(); - - @Key("grid.port") - String gridPort(); - - @Key("faker.locale") - String faker(); -} diff --git a/src/main/java/data/BookingDataFactory.java b/src/main/java/data/BookingDataFactory.java deleted file mode 100644 index a608134..0000000 --- a/src/main/java/data/BookingDataFactory.java +++ /dev/null @@ -1,50 +0,0 @@ -package data; - -import com.aventstack.extentreports.service.ExtentTestManager; -import com.github.javafaker.Faker; -import config.Configuration; -import enums.RoomType; -import java.util.Locale; -import java.util.Random; -import lombok.extern.log4j.Log4j2; -import model.Booking; -import org.aeonbits.owner.ConfigCache; - -@Log4j2 -public class BookingDataFactory { - - private final Faker faker; - - public BookingDataFactory() { - Configuration configuration = ConfigCache.getOrCreate(Configuration.class); - faker = new Faker(new Locale(configuration.faker())); - } - - public Booking createBookingData() { - Booking booking = Booking.builder(). - email(faker.internet().emailAddress()). - country(returnRandomCountry()). - password(faker.internet().password()). - dailyBudget(returnDailyBudget()). - newsletter(faker.bool().bool()). - roomType(RoomType.getRandom()). - roomDescription(faker.lorem().paragraph()). - build(); - - log.info(booking); - ExtentTestManager.getTest().info(booking.toString()); - 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 String returnRandomItemOnArray(String[] array) { - return array[(new Random().nextInt(array.length))]; - } -} diff --git a/src/main/java/driver/IDriver.java b/src/main/java/driver/IDriver.java deleted file mode 100644 index a91f2e4..0000000 --- a/src/main/java/driver/IDriver.java +++ /dev/null @@ -1,8 +0,0 @@ -package driver; - -import org.openqa.selenium.WebDriver; - -public interface IDriver { - - WebDriver createInstance(String browser); -} diff --git a/src/main/java/driver/local/LocalDriver.java b/src/main/java/driver/local/LocalDriver.java deleted file mode 100644 index aab4054..0000000 --- a/src/main/java/driver/local/LocalDriver.java +++ /dev/null @@ -1,59 +0,0 @@ -package driver.local; - -import driver.IDriver; -import io.github.bonigarcia.wdm.DriverManagerType; -import io.github.bonigarcia.wdm.WebDriverManager; -import lombok.extern.log4j.Log4j2; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.edge.EdgeDriver; -import org.openqa.selenium.firefox.FirefoxDriver; -import org.openqa.selenium.ie.InternetExplorerDriver; -import org.openqa.selenium.opera.OperaDriver; - -@Log4j2 -public class LocalDriver implements IDriver { - - @Override - public WebDriver createInstance(String browser) { - WebDriver driver = null; - - try { - DriverManagerType driverManagerType = DriverManagerType.valueOf(browser.toUpperCase()); - Class driverClass = driverResolver(driverManagerType); - WebDriverManager.getInstance(driverManagerType).setup(); - driver = driverClass.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - log.error("Problem during instantiation the driver", e); - } - return driver; - } - - private Class driverResolver(DriverManagerType driverManagerType) { - Class clazz; - - switch (driverManagerType) { - case CHROME: - clazz = ChromeDriver.class; - break; - case FIREFOX: - clazz = FirefoxDriver.class; - break; - case OPERA: - clazz = OperaDriver.class; - break; - case EDGE: - clazz = EdgeDriver.class; - break; - case PHANTOMJS: - case SELENIUM_SERVER_STANDALONE: - throw new IllegalStateException("Not supported: " + driverManagerType); - case IEXPLORER: - clazz = InternetExplorerDriver.class; - break; - default: - throw new IllegalStateException("Unexpected value: " + driverManagerType); - } - return clazz; - } -} diff --git a/src/main/java/driver/remote/RemoteDriver.java b/src/main/java/driver/remote/RemoteDriver.java deleted file mode 100644 index 4acaeb3..0000000 --- a/src/main/java/driver/remote/RemoteDriver.java +++ /dev/null @@ -1,79 +0,0 @@ -package driver.remote; - -import config.Configuration; -import driver.IDriver; -import io.github.bonigarcia.wdm.DriverManagerType; -import lombok.extern.log4j.Log4j2; -import org.aeonbits.owner.ConfigCache; -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; - -import java.net.MalformedURLException; -import java.net.URL; - -@Log4j2 -public class RemoteDriver implements IDriver { - - @Override - public WebDriver createInstance(String browser) { - RemoteWebDriver remoteWebDriver = null; - Configuration configuration = ConfigCache.getOrCreate(Configuration.class); - 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/model/Booking.java b/src/main/java/model/Booking.java deleted file mode 100644 index d894b91..0000000 --- a/src/main/java/model/Booking.java +++ /dev/null @@ -1,24 +0,0 @@ -package model; - -import enums.RoomType; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.ToString; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -public class Booking { - - String email; - String country; - @ToString.Exclude String password; - String dailyBudget; - Boolean newsletter; - RoomType roomType; - String roomDescription; -} diff --git a/src/main/java/test/TestListener.java b/src/main/java/test/TestListener.java deleted file mode 100644 index 344afb4..0000000 --- a/src/main/java/test/TestListener.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 test; - -import com.aventstack.extentreports.service.ExtentTestManager; -import driver.DriverManager; -import lombok.extern.log4j.Log4j2; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; -import org.testng.ITestContext; -import org.testng.ITestListener; -import org.testng.ITestResult; - -@Log4j2 -public class TestListener implements ITestListener { - - @Override - public void onTestStart(ITestResult result) { - ExtentTestManager.getTest().info(DriverManager.getInfo()); - } - - @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()); - - String screenshot = ((TakesScreenshot) DriverManager.getDriver()).getScreenshotAs(OutputType.BASE64); - ExtentTestManager.getTest().addScreenCaptureFromBase64String(screenshot); - } - -} diff --git a/src/main/resources/conf/dev.properties b/src/main/resources/conf/dev.properties deleted file mode 100644 index b41ede6..0000000 --- a/src/main/resources/conf/dev.properties +++ /dev/null @@ -1,15 +0,0 @@ -# target execution: local or grid -target = local - -# initial URL -url.base = https://codepen.io/eliasnogueira/full/PooMNpX - -# global test timeout -timeout = 3 - -# grid url and port -grid.url = localhost -grid.port = 4444 - -# javafaker locale -faker.locale = pt-BR diff --git a/src/main/resources/conf/prod.properties b/src/main/resources/conf/prod.properties deleted file mode 100644 index 7f7d0f7..0000000 --- a/src/main/resources/conf/prod.properties +++ /dev/null @@ -1,15 +0,0 @@ -# target execution: local or grid -target=local - -# initial URL -url.base = https://codepen.io/eliasnogueira/full/PooMNpX - -# global test timeout -timeout = 5 - -# grid url and port -grid.url = 192.168.116.10 -grid.port = 4444 - -# javafaker locale -faker.locale = pt-BR \ No newline at end of file diff --git a/src/main/resources/conf/test.properties b/src/main/resources/conf/test.properties deleted file mode 100644 index 7208e64..0000000 --- a/src/main/resources/conf/test.properties +++ /dev/null @@ -1,15 +0,0 @@ -# target execution: local or grid -target= grid - -# initial URL -url.base = https://codepen.io/eliasnogueira/full/PooMNpX - -# global test timeout -timeout = 5 - -# grid url and port -grid.url = localhost -grid.port = 4444 - -# javafaker locale -faker.locale = pt-BR \ 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 63% rename from src/main/java/test/BaseWeb.java rename to src/test/java/com/eliasnogueira/BaseWeb.java index d18fb41..cd5c894 100644 --- a/src/main/java/test/BaseWeb.java +++ b/src/test/java/com/eliasnogueira/BaseWeb.java @@ -22,39 +22,37 @@ * SOFTWARE. */ -package test; - -import com.aventstack.extentreports.testng.listener.ExtentITestListenerClassAdapter; -import config.Configuration; -import driver.DriverFactory; -import driver.DriverManager; -import org.aeonbits.owner.ConfigCache; -import org.aeonbits.owner.ConfigFactory; +package com.eliasnogueira; + +import com.eliasnogueira.driver.DriverManager; +import com.eliasnogueira.driver.TargetFactory; +import com.eliasnogueira.report.AllureManager; import org.openqa.selenium.WebDriver; -import org.testng.annotations.*; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Optional; +import org.testng.annotations.Parameters; + +import static com.eliasnogueira.config.ConfigurationManager.configuration; -@Listeners({ExtentITestListenerClassAdapter.class, TestListener.class}) public abstract class BaseWeb { @BeforeSuite - @Parameters("environment") - public void setConfiguration(@Optional("dev") String environment) { - String env = System.getenv("environment"); - ConfigFactory.setProperty("env", env == null ? environment : env); + public void beforeSuite() { + AllureManager.setAllureEnvironmentInformation(); } - @BeforeMethod + @BeforeMethod(alwaysRun = true) @Parameters("browser") public void preCondition(@Optional("chrome") String browser) { - Configuration configuration = ConfigCache.getOrCreate(Configuration.class); - - 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 + @AfterMethod(alwaysRun = true) public void postCondition() { DriverManager.quit(); } diff --git a/src/test/java/test/BookRoomWebTest.java b/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java similarity index 62% rename from src/test/java/test/BookRoomWebTest.java rename to src/test/java/com/eliasnogueira/test/BookRoomWebTest.java index 1985689..7471352 100644 --- a/src/test/java/test/BookRoomWebTest.java +++ b/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java @@ -21,37 +21,37 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package test; +package com.eliasnogueira.test; -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.*; +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()) 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/extent.properties b/src/test/resources/extent.properties deleted file mode 100644 index eeedfe6..0000000 --- a/src/test/resources/extent.properties +++ /dev/null @@ -1,2 +0,0 @@ -extent.reporter.html.start=true -extent.reporter.html.out=target/report/execution_report.html 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/test/resources/selenium-grid.properties b/src/test/resources/selenium-grid.properties new file mode 100644 index 0000000..90d2540 --- /dev/null +++ b/src/test/resources/selenium-grid.properties @@ -0,0 +1,3 @@ +# grid url and port +grid.url = localhost +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 70% rename from src/test/resources/suites/multi_browser.xml rename to src/test/resources/suites/selenium-grid.xml index 49d0088..04d9024 100644 --- a/src/test/resources/suites/multi_browser.xml +++ b/src/test/resources/suites/selenium-grid.xml @@ -1,20 +1,18 @@ - - - + - + - \ No newline at end of file +