├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── on-pr.yml │ └── test-execution.yml ├── .gitignore ├── .sdlc ├── .gitlab-ci.yml └── Jenkinsfile ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.MD ├── assets ├── example_filed_test_with_report.gif └── selenium-grid-execution.gif ├── grid ├── config.toml └── docker-compose.yml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── eliasnogueira │ │ ├── config │ │ ├── Configuration.java │ │ └── ConfigurationManager.java │ │ ├── data │ │ ├── changeless │ │ │ └── BrowserData.java │ │ └── dynamic │ │ │ └── BookingDataFactory.java │ │ ├── driver │ │ ├── BrowserFactory.java │ │ ├── DriverManager.java │ │ └── TargetFactory.java │ │ ├── enums │ │ ├── RoomType.java │ │ └── Target.java │ │ ├── exceptions │ │ └── HeadlessNotSupportedException.java │ │ ├── model │ │ └── Booking.java │ │ ├── page │ │ ├── AbstractPageObject.java │ │ └── booking │ │ │ ├── AccountPage.java │ │ │ ├── DetailPage.java │ │ │ ├── RoomPage.java │ │ │ └── common │ │ │ └── NavigationPage.java │ │ └── report │ │ ├── AllureManager.java │ │ └── AllureTestLifecycleListener.java └── resources │ └── log4j2.properties └── test ├── java └── com │ └── eliasnogueira │ ├── BaseWeb.java │ └── test │ └── BookRoomWebTest.java └── resources ├── META-INF └── services │ └── io.qameta.allure.listener.TestLifecycleListener ├── allure.properties ├── general.properties ├── selenium-grid.properties └── suites ├── local.xml └── selenium-grid.xml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Use this to log an issue/bug/improvement 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the issue** 11 | A clear and concise description of the problem 12 | 13 | **Steps** 14 | Steps to reproduce the unexpected behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. See the error 18 | 19 | **Expected behaviour** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Additional information** 23 | Any other relevant information 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '04:00' 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **IMPORTANT: Please do not create a Pull Request without creating an issue first.** 2 | 3 | *Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.* 4 | 5 | Please provide enough information so that others can review your pull request: 6 | 7 | 8 | 9 | Explain the **details** for making this change. What existing problem does the pull request solve? 10 | 11 | 12 | 13 | **Test plan (required)** 14 | 15 | Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. 16 | 17 | 18 | 19 | **Code formatting** 20 | 21 | 22 | 23 | **Closing issues** 24 | 25 | Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). 26 | -------------------------------------------------------------------------------- /.github/workflows/on-pr.yml: -------------------------------------------------------------------------------- 1 | name: On Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up JDK 23 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: oracle 17 | java-version: 23 18 | 19 | - name: Cache Maven packages 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.m2/repository 23 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 24 | restore-keys: | 25 | ${{ runner.os }}-maven- 26 | 27 | - name: Build 28 | run: mvn -q compile 29 | -------------------------------------------------------------------------------- /.github/workflows/test-execution.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | local-test: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up JDK 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: 23 19 | distribution: oracle 20 | 21 | - name: Cache Maven packages 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.m2/repository 25 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 26 | restore-keys: | 27 | ${{ runner.os }}-maven- 28 | 29 | - name: Build with Maven 30 | run: mvn -q -DskipTests package 31 | 32 | - name: Run local tests 33 | run: mvn -q test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .settings/ 2 | grid/assets 3 | 4 | # Intellij 5 | .idea/ 6 | *.iml 7 | 8 | # Maven 9 | logs/ 10 | target/ 11 | 12 | # Allure 13 | .allure -------------------------------------------------------------------------------- /.sdlc/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: maven:3.8.4-openjdk-17 2 | 3 | stages: 4 | - build 5 | - test 6 | 7 | cache: 8 | paths: 9 | - .m2/repository/ 10 | - target/ 11 | 12 | build: 13 | stage: build 14 | script: 15 | - mvn clean package -DskipTests=true 16 | 17 | test: 18 | stage: test 19 | script: 20 | - mvn test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome 21 | -------------------------------------------------------------------------------- /.sdlc/Jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | def mvnHome 3 | 4 | stage('Preparation') { 5 | git 'https://github.com/eliasnogueira/selenium-java-lean-test-achitecture.git' 6 | mvnHome = tool 'M3' 7 | } 8 | 9 | stage('Build') { 10 | sh "'${mvnHome}/bin/mvn' clean package -DskipTests=true" 11 | } 12 | 13 | stage('Test Execution') { 14 | try { 15 | sh "'${mvnHome}/bin/mvn' test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome 16 | } catch (Exception e) { 17 | currentBuild.result = 'FAILURE' 18 | } finally { 19 | junit '**/target/surefire-reports/TEST-*.xml' 20 | 21 | /* 22 | * Please read https://wiki.jenkins.io/display/JENKINS/Configuring+Content+Security+Policy 23 | * to allow Jenkins to load static files 24 | */ 25 | publishHTML (target: [ 26 | allowMissing: false, 27 | alwaysLinkToLastBuild: true, 28 | keepAll: true, 29 | reportDir: 'target/surefire-reports', 30 | reportFiles: 'index.html', 31 | reportName: "TestNG Report" 32 | ]) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at elias.nogueira@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | ## I have an idea or I want to create an issue 4 | If you have an idea, suggestion, feature or an issue, please log an [issue](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/issues). 5 | 6 | Select _Bug report_ if tou want log an issue or _Feature request_ if you want to see something new. 7 | 8 | Do not forget to add a _label_ on the issue or feature. 9 | 10 | ## I have enough knowledge in development to colaborate 11 | Excellent! Thank you to help me out! 12 | 13 | You're going to need a few things first: 14 | * JDK 17+ 15 | * [Configure your IDE](https://projectlombok.org/setup/overview) in order to support Lombok. 16 | 17 | ## Send a pull request 18 | Please add an explanation about the pull request. 19 | If there is or you created an issue/feature, please add the link. 20 | 21 | Pull requests without explanations will be rejected, so please add: 22 | * the reason about you're sending the pull request 23 | * the benefit that your pull request will bring to the application 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Elias Nogueira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Lean Test Automation Architecture using Java and Selenium WebDriver 2 | 3 | [![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) 4 | 5 | **This project delivers to you a complete lean test architecture for your web tests using the best frameworks and 6 | practices.** 7 | 8 | It has a complete solution to run tests in different ways: 9 | 10 | * local testing using the browser on your local machine 11 | * parallel (or single) testing using Selenium Docker 12 | * local testing using TestContainers 13 | * Distributed execution using Selenium Grid 14 | 15 | ## Examples 16 | 17 | ### Local testing execution example 18 | 19 | ![Local testing execution example](assets/example_filed_test_with_report.gif) 20 | 21 | ### Parallel testing execution example with Selenium Grid 22 | 23 | ![Parallel testing execution example with Selenium Grid](assets/selenium-grid-execution.gif) 24 | 25 | ## Languages and Frameworks 26 | 27 | This project uses the following languages and frameworks: 28 | 29 | * [Java 23](https://openjdk.java.net/projects/jdk/23/) as the programming language 30 | * [TestNG](https://testng.org/doc/) as the UnitTest framework to support the test creation 31 | * [Selenium WebDriver](https://www.selenium.dev/) as the web browser automation framework using the Java binding 32 | * [AssertJ](https://joel-costigliola.github.io/assertj/) as the fluent assertion library 33 | * [Allure Report](https://docs.qameta.io/allure/) as the testing report strategy 34 | * [DataFaker](https://www.datafaker.net/) as the faker data generation strategy 35 | * [Log4J2](https://logging.apache.org/log4j/2.x/) as the logging management strategy 36 | * [Owner](http://owner.aeonbits.org/) to minimize the code to handle the properties file 37 | * [TestContainers](https://java.testcontainers.org/modules/webdriver_containers/) Webdriver Containers 38 | 39 | ## Test architecture 40 | 41 | We know that any automation project starts with a good test architecture. 42 | 43 | This project can be your initial test architecture for a faster start. 44 | You will see the following items in this architecture: 45 | 46 | * [Page Objects pattern](#page-objects-pattern) 47 | * [Execution types](#execution-types) 48 | * [BaseTest](#basetest) 49 | * [TestListener](#testlistener) 50 | * [Logging](#logging) 51 | * [Configuration files](#configuration-files) 52 | * [Parallel execution](#parallel-execution) 53 | * [Test Data Factory](#test-data-factory) 54 | * [Profiles executors on pom.xml](#profiles-executors-on-pomxml) 55 | * [Pipeline as a code](#pipeline-as-a-code) 56 | * [Test environment abstraction](#execution-with-docker-selenium-distributed) 57 | 58 | Do you have any other items to add to this test architecture? Please do a pull request or open an issue to discuss. 59 | 60 | ### Page Objects pattern 61 | 62 | I will not explain the Page Object pattern because you can find a lot of good explanations and examples on the internet. 63 | Instead, I will explain what exactly about page objects I'm using in this project. 64 | 65 | #### AbstractPageObject 66 | 67 | This class has a protected constructor to remove the necessity to init the elements using the Page Factory. 68 | Also, it sets the timeout from the `timeout` property value located on `general.properties` file. 69 | 70 | All the Page Object classes should extend the `AbstractPageObject`. 71 | It also tries to remove the `driver` object from the Page Object class as much as possible. 72 | 73 | > **Important information** 74 | > 75 | > There's a `NavigationPage` on the `common` package inside the Page Objects. 76 | > Notice that all the pages extend this one instead of the `AbstractPageObject`. I implemented this way: 77 | > * because the previous and next buttons are fixed on the page (there's no refresh on the page) 78 | > * to avoid creating or passing the new reference to the `NavigationPage` when we need to hit previous or next buttons 79 | 80 | As much as possible avoid this strategy to not get an `ElementNotFoundException` or `StaleElementReferenceException`. 81 | Use this approach if you know that the page does not refresh. 82 | 83 | ### Execution types 84 | 85 | There are different execution types: 86 | 87 | - `local` 88 | - `local-suite` 89 | - `selenium-grid` 90 | - `testcontainers` 91 | 92 | The `TargetFactory` class will resolve the target execution based on the `target` property value located 93 | on `general.properties` file. Its usage is placed on the `BaseWeb` class before each test execution. 94 | 95 | #### Local execution 96 | 97 | ##### Local machine 98 | 99 | **This approach is automatically used when you run the test class in your IDE.** 100 | 101 | When the `target` is `local` the `createLocalDriver()` method is used from the `BrowserFactory` class to return the 102 | browser instance. 103 | 104 | The browser used in the test is placed on the `browser` property in the `general.properties` file. 105 | 106 | ##### Local Suite 107 | 108 | It's the same as the Local Execution, where the difference is that the browser is taken from the TestNG suite file 109 | instead of the `general.properties` 110 | file, enabling you to run multi-browser test approach locally. 111 | 112 | ##### Testcontainers 113 | 114 | This execution type uses the [WebDriver Containers](https://www.testcontainers.org/modules/webdriver_containers/) in 115 | Testcontainers to run the tests in your machine, but using the Selenium docker images for Chrome or Firefox. 116 | 117 | When the `target` is `testcontainers` the `TargetFactory` uses the `createTestContainersInstance()` method to initialize 118 | the container based on the browser set in the `browser` property. Currently, Testcontainers only supports Chrome and 119 | Firefox. 120 | 121 | Example 122 | 123 | ```shell 124 | mvn test -Pweb-execution -Dtarget=testcontainers -Dbrowser=chrome 125 | ``` 126 | 127 | #### Remote execution 128 | 129 | ##### Selenium Grid 130 | 131 | The Selenium Grid approach executes the tests in remote machines (local or remote/cloud grid). 132 | When the `target` is `selenium-grid` the `getOptions` method is used from the `BrowserFactory` to return the browser 133 | option 134 | class as the remote execution needs the browser capability. 135 | 136 | The `DriverFactory` class has an internal method `createRemoteInstance` to return a `RemoteWebDriver` instance based on 137 | the browser capability. 138 | 139 | You must pay attention to the two required information regarding the remote execution: the `grid.url` and `grid.port` 140 | property values on the `grid.properties` file. You must update these values before the start. 141 | 142 | If you are using the `docker-compose.yml` file to start the Docker Selenium grid, the values on the `grid.properties` 143 | file should work. 144 | 145 | You can take a look at the [Execution with Docker Selenium Distributed](#execution-with-docker-selenium-distributed) 146 | to run the parallel tests using this example. 147 | 148 | #### BrowserFactory class 149 | 150 | This Factory class is a Java enum that has all implemented browsers to use during the test execution. 151 | Each browser is an `enum`, and each enum implements four methods: 152 | 153 | * `createLocalDriver()`: creates the browser instance for the local execution. The browser driver is automatically 154 | managed by the WebDriverManager library 155 | * `createDriver()`: creates the browser instance for the remote execution 156 | * `getOptions()`: creates a new browser `Options` setting some specific configurations, and it's used for the remote 157 | executions using the Selenium Grid 158 | * `createTestContainerDriver()` : Creates selenium grid lightweight test container in Standalone mode with 159 | Chrome/Firefox/Edge browser support. 160 | 161 | You can see that the `createLocalDriver()` method use the `getOptions()` to get specific browser configurations, as 162 | starting the browser maximized and others. 163 | 164 | The `getOptions()` is also used for the remote execution as it is a subclass of the `AbstractDriverOptions` and can be 165 | automatically accepted as either a `Capabilities` or `MutableCapabilities` class, which is required by 166 | the `RemoteWebDriver` class. 167 | 168 | #### DriverManager class 169 | 170 | The 171 | class [DriverManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/driver/DriverManager.java) 172 | create a `ThreadLocal` for the WebDriver instance, to make sure there's no conflict when we run it in parallel. 173 | 174 | ### BaseTest 175 | 176 | This testing pattern was implemented on 177 | the [BaseWeb](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/test/java/com/eliasnogueira/BaseWeb.java) 178 | class to automatically run the pre (setup) and post (teardown) conditions. 179 | 180 | The pre-condition uses `@BeforeMethod` from TestNG creates the browser instance based on the values passed either local 181 | or remote execution. 182 | The post-condition uses `@AfterMethod` to close the browser instance. 183 | Both have the `alwaysRun` parameter as `true` to force the run on a pipeline. 184 | 185 | Pay attention that it was designed to open a browser instance to each `@Test` located in the test class. 186 | 187 | This class also has the `TestListener` annotation which is a custom TestNG listener, and will be described in the next 188 | section. 189 | 190 | ### TestListener 191 | 192 | The `TestListener` is a class that 193 | implements [ITestListener](https://testng.org/doc/documentation-main.html#logging-listeners). 194 | The following method is used to help logging errors and attach additional information to the test report: 195 | 196 | * `onTestStart`: add the browser information to the test report 197 | * `onTestFailure`: log the exceptions and add a screenshot to the test report 198 | * `onTestSkipped`: add the skipped test to the log 199 | 200 | ### Logging 201 | 202 | All the log is done by the Log4J using the `@Log4j2` annotation. 203 | 204 | The `log4j2.properties` has two strategies: console and file. 205 | A file with all the log information will be automatically created on the user folder with `test_automation.log` 206 | filename. 207 | If you want to change it, update the `appender.file.fileName` property value. 208 | 209 | The `log.error` is used to log all the exceptions this architecture might throw. Use `log.info` or `log.debug` to log 210 | important information, like the users, automatically generated by the 211 | factory [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) 212 | 213 | ### Parallel execution 214 | 215 | The parallel test execution is based on 216 | the [parallel tests](https://testng.org/doc/documentation-main.html#parallel-tests) 217 | feature on TestNG. This is used by `selenium-grid.xml` test suite file which has the `parallel="tests"` attribute and 218 | value, 219 | whereas `test` item inside the test suite will execute in parallel. 220 | The browser in use for each `test` should be defined by a parameter, like: 221 | 222 | ```xml 223 | 224 | 225 | ``` 226 | 227 | You can define any parallel strategy. 228 | 229 | It can be an excellent combination together with the grid strategy. 230 | 231 | #### Execution with Docker Selenium Distributed 232 | 233 | This project has the `docker-compose.yml` file to run the tests in a parallel way using Docker Selenium. 234 | To be able to run it in parallel the file has 235 | the [Dynamic Grid Implementation](https://github.com/SeleniumHQ/docker-selenium#dynamic-grid-) that will start the 236 | container on demand. 237 | 238 | This means that Docker Selenium will start a container test for a targeting browser. 239 | 240 | Please note that you need to do the following actions before running it in parallel: 241 | 242 | * Docker installed 243 | * Pull the images for Chrome Edge and Firefox - Optional 244 | * Images are pulled if not available and initial test execution will be slow 245 | * `docker pull selenium-standalog-chrome` 246 | * `docker pull selenium-standalog-firefox` 247 | * `docker pull selenium/standalone-edge` 248 | * If you are using a MacBook with either M1 or M2 chip you must check the following experimental feature in Docker 249 | Desktop: Settings -> Features in development -> Use Rosetta for x86/amd64 emulation on Apple Silicon 250 | * Pay attention to the `grid/config.toml` file that has comments for each specific SO 251 | * Start the Grid by running the following command inside the `grid` folder 252 | * `docker-compose up` 253 | * Run the project using the following command 254 | 255 | ```shell 256 | mvn test -Pweb-execution -Dsuite=selenium-grid -Dtarget=selenium-grid -Dheadless=true 257 | ``` 258 | 259 | * Open the [Selenium Grid] page to see the node status 260 | 261 | ### Configuration files 262 | 263 | This project uses a library called [Owner](http://owner.aeonbits.org/). You can find the class related to the property 264 | file reader in the following classes: 265 | 266 | * [Configuration](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/config/Configuration.java) 267 | * [ConfigurationManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/config/ConfigurationManager.java) 268 | 269 | There are 3 properties (configuration) files located on `src/test/java/resources/`: 270 | 271 | * `general.properties`: general configuration as the target execution, browser, base url, timeout, and faker locale 272 | * `grid.properties`: url and port for the Selenium grid usage 273 | 274 | The properties were divided into three different ones to better separate the responsibilities and enable the changes 275 | easy without having a lot of properties inside a single file. 276 | 277 | ### Test Data Factory 278 | 279 | Is the utilization of the Factory design pattern with the Fluent Builder to generate dynamic data. 280 | The [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) 281 | has only one factory `createBookingData` returning a `Booking` object with dynamic data. 282 | 283 | This dynamic data is generated by JavaFaker filling all the fields using the Build pattern. 284 | The [Booking](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/Booking.java) 285 | is the plain Java objects 286 | and 287 | the [BookingBuilder](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/BookingBuilder.java) 288 | is the builder class. 289 | 290 | You can see the usage of the Builder pattern in 291 | the [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) 292 | class. 293 | 294 | Reading reference: https://reflectoring.io/objectmother-fluent-builder 295 | 296 | ### Profiles executors on pom.xml 297 | 298 | There is a profile called `web-execution` created to execute the test suite `local.xml` 299 | inside `src/test/resources/suites` folder. 300 | To execute this suite, via the command line you can call the parameter `-P` and the profile id. 301 | 302 | Eg: executing the multi_browser suite 303 | 304 | ``` bash 305 | mvn test -Pweb-execution 306 | ``` 307 | 308 | If you have more than one suite on _src/test/resources/suites_ folder you can parameterize the xml file name. 309 | To do this you need: 310 | 311 | * Create a property on `pom.xml` called _suite_ 312 | 313 | ```xml 314 | 315 | 316 | local 317 | 318 | ``` 319 | 320 | * Change the profile id 321 | 322 | ```xml 323 | 324 | 325 | web-execution 326 | 327 | ``` 328 | 329 | * Replace the xml file name to `${suite}` on the profile 330 | 331 | ```xml 332 | 333 | 334 | 335 | src/test/resources/suites/${suite}.xml 336 | 337 | 338 | ``` 339 | 340 | * Use `-Dsuite=suite_name` to call the suite 341 | 342 | ````bash 343 | mvn test -Pweb-execution -Dsuite=suite_name 344 | ```` 345 | 346 | ### Pipeline as a code 347 | 348 | The two files of the pipeline as a code are inside `pipeline_as_code` folder. 349 | 350 | * GitHub Actions to use it inside the GitHub located at `.github\workflows` 351 | * Jenkins: `Jenkinsfile` to be used on a Jenkins pipeline located at `pipeline_as_code` 352 | * GitLab CI: `.gitlab-ci.yml` to be used on a GitLab CI `pipeline_as_code` 353 | -------------------------------------------------------------------------------- /assets/example_filed_test_with_report.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasnogueira/selenium-java-lean-test-architecture/d89ec930ffaf38408d2cba7042ee0962fc452ad9/assets/example_filed_test_with_report.gif -------------------------------------------------------------------------------- /assets/selenium-grid-execution.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasnogueira/selenium-java-lean-test-architecture/d89ec930ffaf38408d2cba7042ee0962fc452ad9/assets/selenium-grid-execution.gif -------------------------------------------------------------------------------- /grid/config.toml: -------------------------------------------------------------------------------- 1 | [docker] 2 | # Configs have a mapping between the Docker image to use and the capabilities that need to be matched to 3 | # start a container with the given image. 4 | configs = [ 5 | "selenium/standalone-firefox:latest", "{\"browserName\": \"firefox\"}", 6 | "selenium/standalone-chrome:latest", "{\"browserName\": \"chrome\"}" 7 | ] 8 | 9 | host-config-keys = ["Binds"] 10 | 11 | # URL for connecting to the docker daemon 12 | # Most simple approach, leave it as http://127.0.0.1:2375, and mount /var/run/docker.sock. 13 | # 127.0.0.1 is used because internally the container uses socat when /var/run/docker.sock is mounted 14 | # If var/run/docker.sock is not mounted: 15 | # Windows: make sure Docker Desktop exposes the daemon via tcp, and use http://host.docker.internal:2375. 16 | # macOS: install socat and run the following command, socat -4 TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock, 17 | # then use http://host.docker.internal:2375. 18 | # Linux: varies from machine to machine, please mount /var/run/docker.sock. If this does not work, please create an issue. 19 | 20 | url = "http://host.docker.internal:2375" 21 | # Docker image used for video recording 22 | video-image = "selenium/video:latest" 23 | 24 | # Uncomment the following section if you are running the node on a separate VM 25 | # Fill out the placeholders with appropriate values 26 | #[server] 27 | #host = 28 | #port = 29 | -------------------------------------------------------------------------------- /grid/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | node-docker: 3 | image: selenium/node-docker:latest 4 | volumes: 5 | - ./assets:/opt/selenium/assets 6 | - ./config.toml:/opt/bin/config.toml 7 | - ~/Downloads:/home/seluser/Downloads 8 | - /var/run/docker.sock:/var/run/docker.sock 9 | depends_on: 10 | - selenium-hub 11 | environment: 12 | - SE_EVENT_BUS_HOST=selenium-hub 13 | - SE_EVENT_BUS_PUBLISH_PORT=4442 14 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 15 | 16 | selenium-hub: 17 | image: selenium/hub:latest 18 | container_name: selenium-hub 19 | ports: 20 | - "4442:4442" 21 | - "4443:4443" 22 | - "4444:4444" 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.eliasnogueira 8 | selenium-java-lean-test-architecture 9 | 3.7.0 10 | 11 | 12 | scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git 13 | scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git 14 | 15 | 16 | 17 | 18 | 23 19 | UTF-8 20 | UTF-8 21 | 3.5.3 22 | 3.13.0 23 | 24 | 1.9.23 25 | 4.32.0 26 | 7.11.0 27 | 3.27.2 28 | 2.4.3 29 | 2.24.3 30 | 1.0.12 31 | 2.33.0 32 | 2.29.1 33 | 2.29.1 34 | 2.15.2 35 | 1.0.0 36 | 37 | https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline 38 | 39 | 1.21.0 40 | 2.0.17 41 | 42 | 43 | 1.27.1 44 | 45 | local 46 | 47 | 48 | 49 | 50 | org.seleniumhq.selenium 51 | selenium-java 52 | ${selenium.version} 53 | 54 | 55 | 56 | org.testng 57 | testng 58 | ${testng.version} 59 | 60 | 61 | 62 | org.assertj 63 | assertj-core 64 | ${assertj.version} 65 | 66 | 67 | 68 | net.datafaker 69 | datafaker 70 | ${datafaker.version} 71 | 72 | 73 | 74 | org.apache.logging.log4j 75 | log4j-api 76 | ${log4j.version} 77 | 78 | 79 | 80 | org.apache.logging.log4j 81 | log4j-core 82 | ${log4j.version} 83 | 84 | 85 | 86 | org.apache.logging.log4j 87 | log4j-slf4j-impl 88 | ${log4j.version} 89 | 90 | 91 | 92 | org.aeonbits.owner 93 | owner 94 | ${owner.version} 95 | 96 | 97 | 98 | io.qameta.allure 99 | allure-testng 100 | ${allure-testng.version} 101 | 102 | 103 | 104 | io.qameta.allure 105 | allure-attachments 106 | ${allure-attachments.version} 107 | 108 | 109 | 110 | com.github.automatedowl 111 | allure-environment-writer 112 | ${allure-environment-writer.version} 113 | 114 | 115 | com.google.guava 116 | guava 117 | 118 | 119 | 120 | 121 | 122 | org.testcontainers 123 | selenium 124 | ${testcontainers.selenium.version} 125 | 126 | 127 | org.apache.commons 128 | commons-compress 129 | 130 | 131 | 132 | 133 | 134 | org.slf4j 135 | slf4j-simple 136 | ${slf4j-simple.version} 137 | test 138 | 139 | 140 | 141 | 142 | org.apache.commons 143 | commons-compress 144 | ${commons-compress.version} 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | web-execution 153 | 154 | 155 | 156 | org.apache.maven.plugins 157 | maven-surefire-plugin 158 | ${maven-surefire-plugin.version} 159 | 160 | 161 | src/test/resources/suites/${suite}.xml 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-surefire-plugin 177 | ${maven-surefire-plugin.version} 178 | 179 | 180 | -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" 181 | 182 | false 183 | 184 | 185 | 186 | org.aspectj 187 | aspectjweaver 188 | ${aspectj.version} 189 | 190 | 191 | 192 | 193 | io.qameta.allure 194 | allure-maven 195 | ${allure-maven.version} 196 | 197 | ${allure.version} 198 | 199 | ${allure.cmd.download.url}/${allure.version}/allure-commandline-${allure.version}.zip 200 | 201 | 202 | 203 | 204 | org.apache.maven.plugins 205 | maven-compiler-plugin 206 | ${maven-compiler-plugin.version} 207 | 208 | ${java-compiler.version} 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/config/Configuration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.config; 26 | 27 | import org.aeonbits.owner.Config; 28 | import org.aeonbits.owner.Config.LoadPolicy; 29 | import org.aeonbits.owner.Config.LoadType; 30 | 31 | @LoadPolicy(LoadType.MERGE) 32 | @Config.Sources({ 33 | "system:properties", 34 | "classpath:general.properties", 35 | "classpath:selenium-grid.properties"}) 36 | public interface Configuration extends Config { 37 | 38 | @Key("target") 39 | String target(); 40 | 41 | @Key("browser") 42 | String browser(); 43 | 44 | @Key("headless") 45 | Boolean headless(); 46 | 47 | @Key("url.base") 48 | String url(); 49 | 50 | @Key("timeout") 51 | int timeout(); 52 | 53 | @Key("grid.url") 54 | String gridUrl(); 55 | 56 | @Key("grid.port") 57 | String gridPort(); 58 | 59 | @Key("faker.locale") 60 | String faker(); 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/config/ConfigurationManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.config; 26 | 27 | import org.aeonbits.owner.ConfigCache; 28 | 29 | public class ConfigurationManager { 30 | 31 | private ConfigurationManager() { 32 | } 33 | 34 | public static Configuration configuration() { 35 | return ConfigCache.getOrCreate(Configuration.class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/data/changeless/BrowserData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.data.changeless; 26 | 27 | public final class BrowserData { 28 | 29 | private BrowserData() { 30 | } 31 | 32 | public static final String START_MAXIMIZED = "--start-maximized"; 33 | public static final String DISABLE_INFOBARS = "--disable-infobars"; 34 | public static final String DISABLE_NOTIFICATIONS = "--disable-notifications"; 35 | public static final String REMOTE_ALLOW_ORIGINS = "--remote-allow-origins=*"; 36 | public static final String GENERIC_HEADLESS = "-headless"; 37 | public static final String CHROME_HEADLESS = "--headless=new"; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.data.dynamic; 26 | 27 | import com.eliasnogueira.enums.RoomType; 28 | import com.eliasnogueira.model.Booking; 29 | import net.datafaker.Faker; 30 | import org.apache.logging.log4j.LogManager; 31 | import org.apache.logging.log4j.Logger; 32 | 33 | import java.util.Locale; 34 | 35 | import static com.eliasnogueira.config.ConfigurationManager.configuration; 36 | 37 | public final class BookingDataFactory { 38 | 39 | private static final Faker faker = new Faker(new Locale.Builder().setLanguageTag(configuration().faker()).build()); 40 | private static final Logger logger = LogManager.getLogger(BookingDataFactory.class); 41 | 42 | private BookingDataFactory() { 43 | } 44 | 45 | public static Booking createBookingData() { 46 | var booking = new Booking.BookingBuilder(). 47 | email(faker.internet().emailAddress()). 48 | country(returnRandomCountry()). 49 | password(faker.internet().password()). 50 | dailyBudget(returnDailyBudget()). 51 | newsletter(faker.bool().bool()). 52 | roomType(faker.options().option(RoomType.class)). 53 | roomDescription(faker.lorem().paragraph()). 54 | build(); 55 | 56 | logger.info(booking); 57 | return booking; 58 | } 59 | 60 | private static String returnRandomCountry() { 61 | return faker.options().option("Belgium", "Brazil", "Netherlands"); 62 | } 63 | 64 | private static String returnDailyBudget() { 65 | return faker.options().option("$100", "$100 - $499", "$499 - $999", "$999+"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/driver/BrowserFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.driver; 26 | 27 | import com.eliasnogueira.exceptions.HeadlessNotSupportedException; 28 | import org.openqa.selenium.WebDriver; 29 | import org.openqa.selenium.chrome.ChromeDriver; 30 | import org.openqa.selenium.chrome.ChromeOptions; 31 | import org.openqa.selenium.edge.EdgeDriver; 32 | import org.openqa.selenium.edge.EdgeOptions; 33 | import org.openqa.selenium.firefox.FirefoxDriver; 34 | import org.openqa.selenium.firefox.FirefoxOptions; 35 | import org.openqa.selenium.remote.AbstractDriverOptions; 36 | import org.openqa.selenium.remote.RemoteWebDriver; 37 | import org.openqa.selenium.safari.SafariDriver; 38 | import org.openqa.selenium.safari.SafariOptions; 39 | import org.testcontainers.containers.BrowserWebDriverContainer; 40 | 41 | import static com.eliasnogueira.config.ConfigurationManager.configuration; 42 | import static com.eliasnogueira.data.changeless.BrowserData.CHROME_HEADLESS; 43 | import static com.eliasnogueira.data.changeless.BrowserData.DISABLE_INFOBARS; 44 | import static com.eliasnogueira.data.changeless.BrowserData.DISABLE_NOTIFICATIONS; 45 | import static com.eliasnogueira.data.changeless.BrowserData.GENERIC_HEADLESS; 46 | import static com.eliasnogueira.data.changeless.BrowserData.REMOTE_ALLOW_ORIGINS; 47 | import static com.eliasnogueira.data.changeless.BrowserData.START_MAXIMIZED; 48 | import static java.lang.Boolean.TRUE; 49 | 50 | public enum BrowserFactory { 51 | 52 | CHROME { 53 | @Override 54 | public WebDriver createLocalDriver() { 55 | return new ChromeDriver(getOptions()); 56 | } 57 | 58 | @Override 59 | public WebDriver createTestContainerDriver() { 60 | BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new ChromeOptions()); 61 | driverContainer.start(); 62 | 63 | return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new ChromeOptions()); 64 | } 65 | 66 | @Override 67 | public ChromeOptions getOptions() { 68 | var chromeOptions = new ChromeOptions(); 69 | chromeOptions.addArguments(START_MAXIMIZED); 70 | chromeOptions.addArguments(DISABLE_INFOBARS); 71 | chromeOptions.addArguments(DISABLE_NOTIFICATIONS); 72 | chromeOptions.addArguments(REMOTE_ALLOW_ORIGINS); 73 | 74 | if (configuration().headless()) chromeOptions.addArguments(CHROME_HEADLESS); 75 | 76 | return chromeOptions; 77 | } 78 | }, FIREFOX { 79 | @Override 80 | public WebDriver createLocalDriver() { 81 | return new FirefoxDriver(getOptions()); 82 | } 83 | 84 | @Override 85 | public WebDriver createTestContainerDriver() { 86 | BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new FirefoxOptions()); 87 | driverContainer.start(); 88 | 89 | return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new FirefoxOptions()); 90 | } 91 | 92 | @Override 93 | public FirefoxOptions getOptions() { 94 | var firefoxOptions = new FirefoxOptions(); 95 | firefoxOptions.addArguments(START_MAXIMIZED); 96 | 97 | if (configuration().headless()) firefoxOptions.addArguments(GENERIC_HEADLESS); 98 | 99 | return firefoxOptions; 100 | } 101 | }, EDGE { 102 | @Override 103 | public WebDriver createLocalDriver() { 104 | return new EdgeDriver(getOptions()); 105 | } 106 | 107 | public WebDriver createTestContainerDriver() { 108 | BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new EdgeOptions()); 109 | driverContainer.start(); 110 | 111 | return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new EdgeOptions()); 112 | } 113 | 114 | @Override 115 | public EdgeOptions getOptions() { 116 | var edgeOptions = new EdgeOptions(); 117 | edgeOptions.addArguments(START_MAXIMIZED); 118 | 119 | if (configuration().headless()) edgeOptions.addArguments(GENERIC_HEADLESS); 120 | 121 | return edgeOptions; 122 | } 123 | }, SAFARI { 124 | @Override 125 | public WebDriver createLocalDriver() { 126 | return new SafariDriver(getOptions()); 127 | } 128 | 129 | public WebDriver createTestContainerDriver() { 130 | throw new IllegalArgumentException("Browser Safari not supported on TestContainers yet"); 131 | } 132 | 133 | @Override 134 | public SafariOptions getOptions() { 135 | var safariOptions = new SafariOptions(); 136 | safariOptions.setAutomaticInspection(false); 137 | 138 | if (TRUE.equals(configuration().headless())) 139 | throw new HeadlessNotSupportedException(safariOptions.getBrowserName()); 140 | 141 | return safariOptions; 142 | } 143 | }; 144 | 145 | /** 146 | * Used to run local tests where the WebDriverManager will take care of the driver 147 | * 148 | * @return a new WebDriver instance based on the browser set 149 | */ 150 | public abstract WebDriver createLocalDriver(); 151 | 152 | /** 153 | * @return a new AbstractDriverOptions instance based on the browser set 154 | */ 155 | public abstract AbstractDriverOptions getOptions(); 156 | 157 | /** 158 | * Used to run the remote test execution using Testcontainers 159 | * 160 | * @return a new WebDriver instance based on the browser set 161 | */ 162 | public abstract WebDriver createTestContainerDriver(); 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/driver/DriverManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.driver; 26 | 27 | import org.openqa.selenium.WebDriver; 28 | import org.openqa.selenium.remote.RemoteWebDriver; 29 | 30 | public class DriverManager { 31 | 32 | private static final ThreadLocal driver = new ThreadLocal<>(); 33 | 34 | private DriverManager() {} 35 | 36 | public static WebDriver getDriver() { 37 | return driver.get(); 38 | } 39 | 40 | public static void setDriver(WebDriver driver) { 41 | DriverManager.driver.set(driver); 42 | } 43 | 44 | public static void quit() { 45 | DriverManager.driver.get().quit(); 46 | driver.remove(); 47 | } 48 | 49 | public static String getInfo() { 50 | var cap = ((RemoteWebDriver) DriverManager.getDriver()).getCapabilities(); 51 | String browserName = cap.getBrowserName(); 52 | String platform = cap.getPlatformName().toString(); 53 | String version = cap.getBrowserVersion(); 54 | 55 | return String.format("browser: %s v: %s platform: %s", browserName, version, platform); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/driver/TargetFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.driver; 26 | 27 | import com.eliasnogueira.enums.Target; 28 | import org.apache.logging.log4j.LogManager; 29 | import org.apache.logging.log4j.Logger; 30 | import org.openqa.selenium.MutableCapabilities; 31 | import org.openqa.selenium.WebDriver; 32 | import org.openqa.selenium.remote.RemoteWebDriver; 33 | 34 | import java.net.URI; 35 | 36 | import static com.eliasnogueira.config.ConfigurationManager.configuration; 37 | import static com.eliasnogueira.driver.BrowserFactory.valueOf; 38 | import static java.lang.String.format; 39 | 40 | public class TargetFactory { 41 | 42 | private static final Logger logger = LogManager.getLogger(TargetFactory.class); 43 | 44 | public WebDriver createInstance(String browser) { 45 | Target target = Target.get(configuration().target().toUpperCase()); 46 | 47 | return switch (target) { 48 | case LOCAL -> valueOf(configuration().browser().toUpperCase()).createLocalDriver(); 49 | case LOCAL_SUITE -> valueOf(browser.toUpperCase()).createLocalDriver(); 50 | case SELENIUM_GRID -> createRemoteInstance(valueOf(browser.toUpperCase()).getOptions()); 51 | case TESTCONTAINERS -> valueOf(configuration().browser().toUpperCase()).createTestContainerDriver(); 52 | }; 53 | } 54 | 55 | private RemoteWebDriver createRemoteInstance(MutableCapabilities capability) { 56 | RemoteWebDriver remoteWebDriver = null; 57 | try { 58 | String gridURL = format("http://%s:%s", configuration().gridUrl(), configuration().gridPort()); 59 | 60 | remoteWebDriver = new RemoteWebDriver(URI.create(gridURL).toURL(), capability); 61 | } catch (java.net.MalformedURLException e) { 62 | logger.error("Grid URL is invalid or Grid is not available"); 63 | logger.error("Browser: {}", capability.getBrowserName(), e); 64 | } catch (IllegalArgumentException e) { 65 | logger.error("Browser {} is not valid or recognized", capability.getBrowserName(), e); 66 | } 67 | 68 | return remoteWebDriver; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/enums/RoomType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.enums; 26 | 27 | import java.util.function.Supplier; 28 | 29 | public enum RoomType implements Supplier { 30 | 31 | SINGLE("Single"), FAMILY("Family"), BUSINESS("Business"); 32 | 33 | private final String value; 34 | 35 | RoomType(String value) { 36 | this.value = value; 37 | } 38 | 39 | @Override 40 | public String get() { 41 | return this.value; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/enums/Target.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2022 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.enums; 26 | 27 | import java.util.Collections; 28 | import java.util.Map; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | 31 | import static java.util.Arrays.stream; 32 | import static java.util.stream.Collectors.toMap; 33 | 34 | public enum Target { 35 | 36 | LOCAL("local"), LOCAL_SUITE("local-suite"), SELENIUM_GRID("selenium-grid"), 37 | TESTCONTAINERS("testcontainers"); 38 | 39 | private final String value; 40 | private static final Map ENUM_MAP; 41 | 42 | Target(String value) { 43 | this.value = value; 44 | } 45 | 46 | static { 47 | Map map = stream(Target.values()) 48 | .collect(toMap(instance -> instance.value.toLowerCase(), instance -> instance, (_, b) -> b, ConcurrentHashMap::new)); 49 | ENUM_MAP = Collections.unmodifiableMap(map); 50 | } 51 | 52 | public static Target get(String value) { 53 | if (!ENUM_MAP.containsKey(value.toLowerCase())) 54 | throw new IllegalArgumentException(String.format("Value %s not valid. Use one of the TARGET enum values", value)); 55 | 56 | return ENUM_MAP.get(value.toLowerCase()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/exceptions/HeadlessNotSupportedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.exceptions; 26 | 27 | public class HeadlessNotSupportedException extends IllegalStateException { 28 | 29 | public HeadlessNotSupportedException(String browser) { 30 | super(String.format("Headless not supported for %s browser", browser)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/model/Booking.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.model; 26 | 27 | import com.eliasnogueira.enums.RoomType; 28 | 29 | public record Booking(String email, String country, String password, String dailyBudget, Boolean newsletter, 30 | RoomType roomType, String roomDescription) { 31 | 32 | public static final class BookingBuilder { 33 | 34 | private String email; 35 | private String country; 36 | private String password; 37 | private String dailyBudget; 38 | private Boolean newsletter; 39 | private RoomType roomType; 40 | private String roomDescription; 41 | 42 | public BookingBuilder email(String email) { 43 | this.email = email; 44 | return this; 45 | } 46 | 47 | public BookingBuilder country(String country) { 48 | this.country = country; 49 | return this; 50 | } 51 | 52 | public BookingBuilder password(String password) { 53 | this.password = password; 54 | return this; 55 | } 56 | 57 | public BookingBuilder dailyBudget(String dailyBudget) { 58 | this.dailyBudget = dailyBudget; 59 | return this; 60 | } 61 | 62 | public BookingBuilder newsletter(Boolean newsletter) { 63 | this.newsletter = newsletter; 64 | return this; 65 | } 66 | 67 | public BookingBuilder roomType(RoomType roomType) { 68 | this.roomType = roomType; 69 | return this; 70 | } 71 | 72 | public BookingBuilder roomDescription(String roomDescription) { 73 | this.roomDescription = roomDescription; 74 | return this; 75 | } 76 | 77 | public Booking build() { 78 | return new Booking(email, country, password, dailyBudget, newsletter, roomType, roomDescription); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/page/AbstractPageObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.page; 26 | 27 | import com.eliasnogueira.driver.DriverManager; 28 | import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory; 29 | 30 | import static com.eliasnogueira.config.ConfigurationManager.configuration; 31 | import static org.openqa.selenium.support.PageFactory.initElements; 32 | 33 | public class AbstractPageObject { 34 | 35 | protected AbstractPageObject() { 36 | initElements(new AjaxElementLocatorFactory(DriverManager.getDriver(), configuration().timeout()), this); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/page/booking/AccountPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.page.booking; 26 | 27 | import com.eliasnogueira.page.booking.common.NavigationPage; 28 | import io.qameta.allure.Step; 29 | import org.openqa.selenium.WebElement; 30 | import org.openqa.selenium.support.FindBy; 31 | import org.openqa.selenium.support.ui.Select; 32 | 33 | public class AccountPage extends NavigationPage { 34 | 35 | @FindBy(id = "email") 36 | private WebElement email; 37 | 38 | @FindBy(id = "password") 39 | private WebElement password; 40 | 41 | @FindBy(name = "country") 42 | private WebElement country; 43 | 44 | @FindBy(name = "budget") 45 | private WebElement budget; 46 | 47 | @FindBy(css = ".check") 48 | private WebElement newsletter; 49 | 50 | @Step 51 | public void fillEmail(String email) { 52 | this.email.sendKeys(email); 53 | } 54 | 55 | @Step 56 | public void fillPassword(String password) { 57 | this.password.sendKeys(password); 58 | } 59 | 60 | @Step 61 | public void selectCountry(String country) { 62 | new Select(this.country).selectByVisibleText(country); 63 | } 64 | 65 | @Step 66 | public void selectBudget(String value) { 67 | new Select(budget).selectByVisibleText(value); 68 | } 69 | 70 | @Step 71 | public void clickNewsletter() { 72 | newsletter.click(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/page/booking/DetailPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.page.booking; 26 | 27 | import com.eliasnogueira.driver.DriverManager; 28 | import com.eliasnogueira.page.booking.common.NavigationPage; 29 | import io.qameta.allure.Step; 30 | import org.openqa.selenium.WebElement; 31 | import org.openqa.selenium.interactions.Actions; 32 | import org.openqa.selenium.support.FindBy; 33 | 34 | public class DetailPage extends NavigationPage { 35 | 36 | @FindBy(id = "description") 37 | private WebElement roomDescription; 38 | 39 | @FindBy(css = "#message > p") 40 | private WebElement message; 41 | 42 | @Step 43 | public void fillRoomDescription(String description) { 44 | new Actions(DriverManager.getDriver()).sendKeys(roomDescription, description); 45 | } 46 | 47 | @Step 48 | public String getAlertMessage() { 49 | return message.getText(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/page/booking/RoomPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.page.booking; 26 | 27 | import com.eliasnogueira.driver.DriverManager; 28 | import com.eliasnogueira.page.booking.common.NavigationPage; 29 | import io.qameta.allure.Step; 30 | import org.openqa.selenium.By; 31 | 32 | public class RoomPage extends NavigationPage { 33 | 34 | @Step 35 | public void selectRoomType(String room) { 36 | DriverManager.getDriver().findElement(By.xpath("//h6[text()='" + room + "']")).click(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.page.booking.common; 26 | 27 | import com.eliasnogueira.page.AbstractPageObject; 28 | import io.qameta.allure.Step; 29 | import org.openqa.selenium.WebElement; 30 | import org.openqa.selenium.support.FindBy; 31 | 32 | public class NavigationPage extends AbstractPageObject { 33 | 34 | @FindBy(name = "next") 35 | private WebElement next; 36 | 37 | @FindBy(name = "previous") 38 | private WebElement previous; 39 | 40 | @FindBy(name = "finish") 41 | private WebElement finish; 42 | 43 | @Step 44 | public void next() { 45 | next.click(); 46 | } 47 | 48 | @Step 49 | public void finish() { 50 | finish.click(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/report/AllureManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira.report; 26 | 27 | import com.eliasnogueira.enums.Target; 28 | import com.github.automatedowl.tools.AllureEnvironmentWriter; 29 | import com.google.common.collect.ImmutableMap; 30 | 31 | import java.util.HashMap; 32 | import java.util.Map; 33 | 34 | import static com.eliasnogueira.config.ConfigurationManager.configuration; 35 | 36 | public class AllureManager { 37 | 38 | private AllureManager() { 39 | } 40 | 41 | public static void setAllureEnvironmentInformation() { 42 | var basicInfo = new HashMap<>(Map.of( 43 | "Test URL", configuration().url(), 44 | "Target execution", configuration().target(), 45 | "Global timeout", String.valueOf(configuration().timeout()), 46 | "Headless mode", String.valueOf(configuration().headless()), 47 | "Faker locale", configuration().faker(), 48 | "Local browser", configuration().browser() 49 | )); 50 | 51 | if (configuration().target().equals(Target.SELENIUM_GRID.name())) { 52 | var gridMap = Map.of("Grid URL", configuration().gridUrl(), "Grid port", configuration().gridPort()); 53 | basicInfo.putAll(gridMap); 54 | } 55 | 56 | AllureEnvironmentWriter.allureEnvironmentWriter(ImmutableMap.copyOf(basicInfo)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2024 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.eliasnogueira.report; 25 | 26 | import com.eliasnogueira.driver.DriverManager; 27 | import io.qameta.allure.Attachment; 28 | import io.qameta.allure.listener.TestLifecycleListener; 29 | import io.qameta.allure.model.TestResult; 30 | import org.openqa.selenium.OutputType; 31 | import org.openqa.selenium.TakesScreenshot; 32 | import org.openqa.selenium.WebDriver; 33 | 34 | import static io.qameta.allure.model.Status.BROKEN; 35 | import static io.qameta.allure.model.Status.FAILED; 36 | 37 | /* 38 | * Approach implemented using the https://github.com/biczomate/allure-testng7.5-attachment-example as reference 39 | */ 40 | public class AllureTestLifecycleListener implements TestLifecycleListener { 41 | 42 | public AllureTestLifecycleListener() { 43 | } 44 | 45 | @Attachment(value = "Page Screenshot", type = "image/png") 46 | public byte[] saveScreenshot(WebDriver driver) { 47 | return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); 48 | } 49 | 50 | @Override 51 | public void beforeTestStop(TestResult result) { 52 | if (FAILED == result.getStatus() || BROKEN == result.getStatus()) { 53 | saveScreenshot(DriverManager.getDriver()); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | name=PropertiesConfig 2 | property.filename = logs 3 | appenders = console, file 4 | 5 | appender.console.type = Console 6 | appender.console.name = STDOUT 7 | appender.console.layout.type = PatternLayout 8 | appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n 9 | 10 | appender.file.type = File 11 | appender.file.name = LOGFILE 12 | appender.file.fileName=${sys:user.home}/test_automation.log 13 | appender.file.layout.type=PatternLayout 14 | appender.file.layout.pattern=[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n 15 | 16 | rootLogger.level = info 17 | rootLogger.appenderRefs = stdout 18 | rootLogger.appenderRef.stdout.ref = STDOUT 19 | rootLogger.appenderRef.file.ref = LOGFILE 20 | -------------------------------------------------------------------------------- /src/test/java/com/eliasnogueira/BaseWeb.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.eliasnogueira; 26 | 27 | import com.eliasnogueira.driver.DriverManager; 28 | import com.eliasnogueira.driver.TargetFactory; 29 | import com.eliasnogueira.report.AllureManager; 30 | import org.openqa.selenium.WebDriver; 31 | import org.testng.annotations.AfterMethod; 32 | import org.testng.annotations.BeforeMethod; 33 | import org.testng.annotations.BeforeSuite; 34 | import org.testng.annotations.Optional; 35 | import org.testng.annotations.Parameters; 36 | 37 | import static com.eliasnogueira.config.ConfigurationManager.configuration; 38 | 39 | public abstract class BaseWeb { 40 | 41 | @BeforeSuite 42 | public void beforeSuite() { 43 | AllureManager.setAllureEnvironmentInformation(); 44 | } 45 | 46 | @BeforeMethod(alwaysRun = true) 47 | @Parameters("browser") 48 | public void preCondition(@Optional("chrome") String browser) { 49 | WebDriver driver = new TargetFactory().createInstance(browser); 50 | DriverManager.setDriver(driver); 51 | 52 | DriverManager.getDriver().get(configuration().url()); 53 | } 54 | 55 | @AfterMethod(alwaysRun = true) 56 | public void postCondition() { 57 | DriverManager.quit(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/eliasnogueira/test/BookRoomWebTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Elias Nogueira 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.eliasnogueira.test; 25 | 26 | import com.eliasnogueira.BaseWeb; 27 | import com.eliasnogueira.data.dynamic.BookingDataFactory; 28 | import com.eliasnogueira.page.booking.AccountPage; 29 | import com.eliasnogueira.page.booking.DetailPage; 30 | import com.eliasnogueira.page.booking.RoomPage; 31 | import org.testng.annotations.Test; 32 | 33 | import static org.assertj.core.api.Assertions.assertThat; 34 | 35 | public class BookRoomWebTest extends BaseWeb { 36 | 37 | @Test(description = "Book a room") 38 | public void bookARoom() { 39 | var bookingInformation = BookingDataFactory.createBookingData(); 40 | 41 | var accountPage = new AccountPage(); 42 | accountPage.fillEmail(bookingInformation.email()); 43 | accountPage.fillPassword(bookingInformation.password()); 44 | accountPage.selectCountry(bookingInformation.country()); 45 | accountPage.selectBudget(bookingInformation.dailyBudget()); 46 | accountPage.clickNewsletter(); 47 | accountPage.next(); 48 | 49 | var roomPage = new RoomPage(); 50 | roomPage.selectRoomType(bookingInformation.roomType().get()); 51 | roomPage.next(); 52 | 53 | var detailPage = new DetailPage(); 54 | detailPage.fillRoomDescription(bookingInformation.roomDescription()); 55 | detailPage.finish(); 56 | 57 | assertThat(detailPage.getAlertMessage()) 58 | .isEqualTo("Your reservation has been made and we will contact you shortly"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener: -------------------------------------------------------------------------------- 1 | com.eliasnogueira.report.AllureTestLifecycleListener 2 | -------------------------------------------------------------------------------- /src/test/resources/allure.properties: -------------------------------------------------------------------------------- 1 | allure.results.directory=target/allure-results 2 | -------------------------------------------------------------------------------- /src/test/resources/general.properties: -------------------------------------------------------------------------------- 1 | # target execution: local, selenium-grid or testcontainers 2 | target = local 3 | 4 | # browser to use for local and testcontainers execution 5 | browser = chrome 6 | 7 | # initial URL 8 | url.base = https://eliasnogueira.com/external/selenium-java-architecture/ 9 | 10 | # global test timeout 11 | timeout = 3 12 | 13 | # datafaker locale 14 | faker.locale = en-US 15 | 16 | # headless mode only for chrome or firefox and local execution 17 | headless = false 18 | -------------------------------------------------------------------------------- /src/test/resources/selenium-grid.properties: -------------------------------------------------------------------------------- 1 | # grid url and port 2 | grid.url = localhost 3 | grid.port = 4444 4 | -------------------------------------------------------------------------------- /src/test/resources/suites/local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/suites/selenium-grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------