├── .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 | [](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 | 
20 |
21 | ### Parallel testing execution example with Selenium Grid
22 |
23 | 
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 |
--------------------------------------------------------------------------------