├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── scripts │ └── InstallChrome.sh └── workflows │ └── run.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── common ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── cmccarthy │ │ └── common │ │ ├── service │ │ ├── RestService.java │ │ └── StepDefinitionDataManager.java │ │ └── utils │ │ ├── ApplicationProperties.java │ │ ├── Constants.java │ │ ├── CustomEventEvaluator.java │ │ ├── DateTimeUtil.java │ │ ├── HookUtil.java │ │ ├── LogManager.java │ │ └── StringUtil.java │ └── resources │ ├── allure.properties │ ├── application-cloud-provider.properties │ ├── application-dev.properties │ ├── application-headless-github.properties │ ├── application-prod.properties │ ├── application-uat.properties │ ├── application.properties │ ├── demo │ ├── allure-report.png │ └── extent-report.jpg │ ├── downloadDriver.sh │ ├── extent.properties │ ├── extent.xml │ └── logback.xml ├── pom.xml ├── weather ├── pom.xml └── src │ └── test │ ├── java │ └── com │ │ └── cmccarthy │ │ └── api │ │ ├── WeatherRunnerTest.java │ │ ├── config │ │ ├── WeatherAbstractTestDefinition.java │ │ └── WeatherContextConfiguration.java │ │ ├── model │ │ └── response │ │ │ ├── Clouds.java │ │ │ ├── Coord.java │ │ │ ├── LocationWeatherRootResponse.java │ │ │ ├── Main.java │ │ │ ├── Sys.java │ │ │ ├── Weather.java │ │ │ └── Wind.java │ │ ├── service │ │ └── WeatherService.java │ │ └── step │ │ ├── Hooks.java │ │ └── WeatherStep.java │ └── resources │ ├── cucumber.properties │ ├── feature │ ├── WeatherTest.feature │ └── WeatherTest2.feature │ └── suite │ └── WeatherSuiteTest.xml └── wikipedia ├── pom.xml └── src └── test ├── java └── com │ └── cmccarthy │ └── ui │ ├── WikipediaParallelRunnerTest.java │ ├── annotations │ └── PageObject.java │ ├── config │ ├── WikipediaAbstractTestDefinition.java │ └── WikipediaContextConfiguration.java │ ├── page │ ├── AbstractPage.java │ ├── WikipediaCommonPage.java │ └── WikipediaHomePage.java │ ├── step │ ├── AbstractStep.java │ ├── CommonPageSteps.java │ ├── Hooks.java │ └── WikipediaPageSteps.java │ └── utils │ ├── DriverHelper.java │ ├── DriverManager.java │ ├── DriverWait.java │ └── expectedConditions │ ├── ClickabilityOfElement.java │ ├── ClickabilityOfElementByLocator.java │ ├── InvisibilityOfElement.java │ ├── InvisibilityOfElementByLocator.java │ ├── VisibilityOfElement.java │ └── VisibilityOfElementByLocator.java └── resources ├── cucumber.properties ├── feature ├── WikipediaTest.feature └── WikipediaTest2.feature └── suite └── WikipediaSuiteTest.xml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.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/scripts/InstallChrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo apt-get install xvfb 3 | 4 | sudo apt-get install unzip 5 | 6 | wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 7 | sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' 8 | sudo apt-get update 9 | sudo apt install google-chrome-stable -------------------------------------------------------------------------------- /.github/workflows/run.yml: -------------------------------------------------------------------------------- 1 | name: run 2 | 3 | on: 4 | pull_request: 5 | branches: [ "master" ] 6 | push: 7 | branches: [ "feature/WebDriverManager" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: maven 22 | - name: Remove Chrome 23 | run: sudo apt purge google-chrome-stable 24 | - name: Remove default Chromium 25 | run: sudo apt purge chromium-browser 26 | 27 | - name: Install Google Chrome # Using shell script to install Google Chrome 28 | run: | 29 | chmod +x ./.github/scripts/InstallChrome.sh 30 | ./.github/scripts/InstallChrome.sh 31 | 32 | - run: | 33 | export DISPLAY=:99 34 | chromedriver --url-base=/wd/hub & 35 | sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional 36 | 37 | - name: Test 38 | run: mvn clean install -DactiveProfile=headless-github -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | screenshots/ 2 | *.log 3 | target/ 4 | test-output/ 5 | .idea 6 | *.iml 7 | .allure 8 | .iml 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Guidelines 2 | 3 | Thank you for considering contributing to our project! We appreciate your interest in improving our repository and welcome any contributions you may have. 4 | 5 | To ensure a smooth and collaborative experience for everyone, we have outlined some guidelines for contributing to this project. Please take a moment to review these guidelines before making your contribution. 6 | 7 | ## How to Contribute 8 | 9 | 1. **Fork the Repository**: Fork the repository to your own GitHub account. 10 | 11 | 2. **Clone the Repository**: Clone the forked repository to your local machine using the following command: 12 | ``` 13 | git clone https://github.com/your-username/repository.git 14 | ``` 15 | 16 | 3. **Create a Branch**: Create a new branch to work on your feature or bug fix using a descriptive name: 17 | ``` 18 | git checkout -b feature/your-feature 19 | ``` 20 | 21 | 4. **Make Changes**: Make your changes within the created branch. Ensure that your code follows the existing code style and conventions. 22 | 23 | 5. **Commit Changes**: Commit your changes with a descriptive commit message: 24 | ``` 25 | git commit -am 'Add a brief description of your changes' 26 | ``` 27 | 28 | 6. **Push Changes**: Push your changes to your forked repository: 29 | ``` 30 | git push origin feature/your-feature 31 | ``` 32 | 33 | 7. **Submit a Pull Request (PR)**: Once you have pushed your changes to your forked repository, navigate to the original repository and submit a pull request. Be sure to include a detailed description of the changes you have made. 34 | 35 | 8. **Code Review**: Your pull request will undergo review by the maintainers. Be prepared to address any feedback or suggestions. 36 | 37 | 9. **Merge**: Once your pull request has been approved and all feedback has been addressed, it will be merged into the main branch. 38 | 39 | 40 | ## Reporting Bugs 41 | 42 | If you encounter any bugs or issues while using our project, please report them by opening an issue in the GitHub repository. Include as much detail as possible, including steps to reproduce the issue. 43 | 44 | ## Feature Requests 45 | 46 | We welcome suggestions for new features or improvements. If you have an idea for enhancing our project, please open an issue in the GitHub repository to discuss it. 47 | 48 | ## Additional Resources 49 | 50 | - [GitHub Flow Guide](https://guides.github.com/introduction/flow/) 51 | - [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) 52 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 53 | - [Markdown Syntax Guide](https://www.markdownguide.org/basic-syntax/) 54 | 55 | Thank you for your contributions! 56 | 57 | *(Please replace "your-username" and "repository" with your actual GitHub username and repository name in the commands above.)* 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 cmccarthyIrl 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Automated Testing Framework 2 | 3 | [![run](https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/actions/workflows/run.yml/badge.svg)](https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/actions/workflows/run.yml) 4 | 5 | # Index 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 40 | 41 |
Start 10 | | Maven 11 | | Quickstart | 12 |
Run 17 | | TestNG 18 | | Command Line 19 | | IDE Support 20 | | Java JDK 21 | | Troubleshooting | 22 |
Report 27 | | Configuration 28 | | Environment Switching 29 | | Spark HTML Reports 30 | | Logging | 31 |
Advanced 36 | | Before / After Hooks 37 | | JSON Transforms 38 | | Contributing | 39 |
42 | 43 | # Maven 44 | 45 | The Framework uses [Spring Boot Test](https://spring.io/guides/gs/testing-web/), [Cucumber](https://cucumber.io/) 46 | , [Rest Assured](https://rest-assured.io/) and [Selenium](https://www.selenium.dev/) client implementations. 47 | 48 | Spring ``: 49 | 50 | ```xml 51 | 52 | 53 | ... 54 | 55 | org.springframework.amqp 56 | spring-rabbit 57 | ${spring-rabbit.version} 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-test 62 | 63 | 64 | org.springframework 65 | spring-test 66 | 67 | ... 68 | 69 | ``` 70 | 71 | Cucumber & Rest Assured ``: 72 | 73 | ```xml 74 | 75 | 76 | ... 77 | 78 | io.rest-assured 79 | rest-assured 80 | ${restassured.version} 81 | 82 | 83 | io.cucumber 84 | cucumber-java 85 | ${cucumber.version} 86 | 87 | 88 | io.cucumber 89 | cucumber-spring 90 | ${cucumber.version} 91 | 92 | 93 | io.cucumber 94 | cucumber-testng 95 | ${cucumber.version} 96 | 97 | ... 98 | 99 | ``` 100 | 101 | Selenium ``: 102 | 103 | ```xml 104 | 105 | 106 | ... 107 | 108 | org.seleniumhq.selenium 109 | selenium-java 110 | ${selenium-version} 111 | 112 | 113 | org.seleniumhq.selenium 114 | selenium-server 115 | ${selenium-version} 116 | 117 | ... 118 | 119 | ``` 120 | 121 | # Quickstart 122 | 123 | - [Intellij IDE](https://www.jetbrains.com/idea/) - `Recommended` 124 | - [Java JDK 17](https://jdk.java.net/java-se-ri/11) 125 | - [Apache Maven](https://maven.apache.org/docs/3.6.3/release-notes.html) 126 | 127 | # TestNG 128 | 129 | By using the [TestNG Framework](https://junit.org/junit4/) we can utilize the [Cucumber Framework](https://cucumber.io/) 130 | and the `@CucumberOptions` Annotation Type to execute the `*.feature` file tests 131 | 132 | > Right click the `WikipediParallelRunner` class and select `Run` 133 | 134 | ```java 135 | 136 | @CucumberOptions( 137 | features = { 138 | "src/test/resources/feature" 139 | }, 140 | plugin = { 141 | "pretty", 142 | "json:target/cucumber/report.json", 143 | "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:" 144 | }) 145 | public class WikipediaParallelRunnerTest extends AbstractTestNGCucumberTests { 146 | 147 | @Override 148 | @DataProvider(parallel = true) 149 | public Object[][] scenarios() { 150 | return super.scenarios(); 151 | } 152 | 153 | } 154 | ``` 155 | 156 | # Command Line 157 | 158 | Normally you will use your IDE to run a `*.feature` file directly or via the `*Test.java` class. With the `Test` class, 159 | we can run tests from the command-line as well. 160 | 161 | Note that the `mvn test` command only runs test classes that follow the `*Test.java` naming convention. 162 | 163 | You can run a single test or a suite or tests like so : 164 | 165 | ``` 166 | mvn test -Dtest=WikipediaParallelRunnerTest 167 | ``` 168 | 169 | Note that the `mvn clean install` command runs all test Classes that follow the `*Test.java` naming convention 170 | 171 | ``` 172 | mvn clean install 173 | ``` 174 | 175 | # IDE Support 176 | 177 | To minimize the discrepancies between IDE versions and Locales the `` is set to `UTF-8` 178 | 179 | ```xml 180 | 181 | 182 | ... 183 | UTF-8 184 | UTF-8 185 | ... 186 | 187 | ``` 188 | 189 | # Java JDK 190 | 191 | The Java version to use is defined in the `maven-compiler-plugin` 192 | 193 | ```xml 194 | 195 | 196 | ... 197 | 198 | 199 | ... 200 | 201 | org.apache.maven.plugins 202 | maven-compiler-plugin 203 | 204 | 17 205 | 17 206 | 207 | 208 | ... 209 | 210 | 211 | ... 212 | 213 | ``` 214 | 215 | # Configuration 216 | 217 | The `AbstractTestDefinition` class is responsible for specifying each Step class as `@SpringBootTest` and 218 | its `@ContextConfiguration` 219 | 220 | ```java 221 | 222 | @ContextConfiguration(classes = {FrameworkContextConfiguration.class}) 223 | @SpringBootTest 224 | public class AbstractTestDefinition { 225 | } 226 | ``` 227 | 228 | The `FrameworkContextConfiguration` class is responsible for specifying the Spring `@Configuration`, modules to scan, 229 | properties to use etc 230 | 231 | ```java 232 | 233 | @EnableRetry 234 | @Configuration 235 | @ComponentScan({ 236 | "com.cmccarthy.api", "com.cmccarthy.common", 237 | }) 238 | @PropertySource("application.properties") 239 | public class FrameworkContextConfiguration { 240 | } 241 | ``` 242 | 243 | # Environment Switching 244 | 245 | There is only one thing you need to do to switch the environment - which is to set `` property in the 246 | Master POM. 247 | 248 | > By default, the value of `spring.profiles.active` is defined in the `application.properties` file which inherits its 249 | > value from the Master POM property `` 250 | 251 | ```xml 252 | 253 | 254 | ... 255 | 256 | prod 257 | 258 | true 259 | 260 | 261 | prod 262 | 263 | 264 | ... 265 | 266 | ``` 267 | 268 | You can then specify the profile to use when running Maven from the command line like so: 269 | 270 | ``` 271 | mvn clean install -DactiveProfile=github-headless 272 | ``` 273 | 274 | Below is an example of the `application.properties` file. 275 | 276 | ```properties 277 | spring.profiles.active=@activatedProperties@ 278 | ``` 279 | 280 | # Extent Reports 281 | 282 | The Framework uses [Extent Reports Framework](https://extentreports.com/) to generate the HTML Test Reports 283 | 284 | The example below is a report generated automatically by Extent Reports open-source library. 285 | 286 | 287 | 288 | # Allure Reports 289 | 290 | The Framework uses [Allure Reports](https://docs.qameta.io/allure/) to generate the HTML Test Reports 291 | 292 | The example below is a report generated by Allure Reports open-source library. 293 | 294 | 295 | 296 | To generate the above report navigate to the root directory of the module under test and execute the following command 297 | 298 | `mvn allure:serve` or `mvn allure:generate` (for an offline report) 299 | 300 | # Logging 301 | 302 | The Framework uses [Log4j2](https://logging.apache.org/log4j/2.x/) You can instantiate the logging service in any Class 303 | like so 304 | 305 | ```java 306 | private final Logger logger=LoggerFactory.getLogger(WikipediaPageSteps.class); 307 | ``` 308 | 309 | you can then use the logger like so : 310 | 311 | ```java 312 | logger.info("This is a info message"); 313 | logger.warn("This is a warning message"); 314 | logger.debug("This is a info message"); 315 | logger.error("This is a error message"); 316 | ``` 317 | 318 | # Before / After Hooks 319 | 320 | The [Logback](http://logback.qos.ch/) logging service is initialized from the `Hooks.class` 321 | 322 | As the Cucumber Hooks are implemented by all steps we can configure the `@CucumberContextConfiguration` like so : 323 | 324 | ```java 325 | 326 | @CucumberContextConfiguration 327 | public class Hooks extends AbstractTestDefinition { 328 | 329 | private static boolean initialized = false; 330 | private static final Object lock = new Object(); 331 | 332 | @Autowired 333 | private HookUtil hookUtil; 334 | @Autowired 335 | private DriverManager driverManager; 336 | 337 | @Before 338 | public void beforeScenario(Scenario scenario) { 339 | synchronized (lock) { 340 | if (!initialized) { 341 | if (!driverManager.isDriverExisting()) { 342 | driverManager.downloadDriver(); 343 | } 344 | initialized = true; 345 | } 346 | } 347 | driverManager.createDriver(); 348 | } 349 | 350 | @After 351 | public void afterScenario(Scenario scenario) { 352 | hookUtil.endOfTest(scenario); 353 | WebDriverRunner.closeWebDriver(); 354 | } 355 | } 356 | ``` 357 | 358 | # JSON Transforms 359 | 360 | [Rest Assured IO](https://rest-assured.io/) is used to map the `Response` Objects to their respective `POJO` Classes 361 | 362 | ```xml 363 | 364 | 365 | io.rest-assured 366 | rest-assured 367 | 3.0.0 368 | 369 | ``` 370 | 371 | # Troubleshooting 372 | 373 | - Execute the following commands to resolve any dependency issues 374 | 1. `cd ~/install directory path/spring-cucumber-testng-parallel-test-harness` 375 | 2. `mvn clean install -DskipTests` 376 | 377 | # Contributing 378 | 379 | Spotted a mistake? Questions? Suggestions? 380 | 381 | [Open an Issue](https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/issues) 382 | 383 | 384 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cucumber-testng-parallel-test-harness 7 | spring-cucumber-testng-parallel-test-harness 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 12 | 4.0.0 13 | 1.0-SNAPSHOT 14 | common 15 | 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/service/RestService.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.service; 2 | 3 | import io.restassured.specification.RequestSpecification; 4 | import org.springframework.stereotype.Service; 5 | 6 | import static io.restassured.RestAssured.given; 7 | 8 | @Service 9 | public class RestService { 10 | 11 | public RequestSpecification getRequestSpecification() { 12 | return given().header("Content-Type", "application/json"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/service/StepDefinitionDataManager.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | @Service 9 | public class StepDefinitionDataManager { 10 | 11 | private final ThreadLocal> storedObjectMap = new ThreadLocal<>(); 12 | 13 | public Map getStoredObjectMap() { 14 | return storedObjectMap.get(); 15 | } 16 | 17 | public void addToStoredObjectMap(String value, Object object) { 18 | final Map tempObjectMap; 19 | if (getStoredObjectMap() == null) { 20 | tempObjectMap = new HashMap<>(); 21 | } else { 22 | tempObjectMap = getStoredObjectMap(); 23 | tempObjectMap.remove(value); 24 | } 25 | tempObjectMap.put(value, object); 26 | storedObjectMap.set(tempObjectMap); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/ApplicationProperties.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class ApplicationProperties { 8 | 9 | @Value("${weather.url.value}") 10 | private String weatherAppUrl; 11 | @Value("${wikipedia.url.value}") 12 | private String wikipediaUrl; 13 | @Value("${browser}") 14 | private String browser; 15 | @Value("${gridUrl}") 16 | private String gridUrl; 17 | 18 | public String getWeatherAppUrl() { 19 | return weatherAppUrl; 20 | } 21 | 22 | public void setWeatherAppUrl(String weatherAppUrl) { 23 | this.weatherAppUrl = weatherAppUrl; 24 | } 25 | 26 | public String getWikipediaUrl() { 27 | return wikipediaUrl; 28 | } 29 | 30 | public void setWikipediaUrl(String wikipediaUrl) { 31 | this.wikipediaUrl = wikipediaUrl; 32 | } 33 | 34 | public String getBrowser() { 35 | return browser; 36 | } 37 | 38 | public void setBrowser(String browser) { 39 | this.browser = browser; 40 | } 41 | 42 | public String getGridUrl() { 43 | return gridUrl; 44 | } 45 | } -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | public class Constants { 4 | 5 | public static final long timeoutLong = 30; 6 | 7 | public static final long pollingLong = 200; 8 | 9 | public static final long timeoutShort = 10; 10 | 11 | public static final long pollingShort = 100; 12 | 13 | public static String DRIVER_DIRECTORY = System.getProperty("user.dir") + "/src/test/resources/drivers"; 14 | 15 | public static String COMMON_RESOURCES = System.getProperty("user.dir") + "/../common/src/main/resources"; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/CustomEventEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.boolex.EventEvaluatorBase; 5 | 6 | public class CustomEventEvaluator extends EventEvaluatorBase { 7 | 8 | @Override 9 | public boolean evaluate(ILoggingEvent event) { 10 | // Check if logger name contains 'com.cmccarthy' 11 | return event.getLoggerName().contains("com.cmccarthy"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/DateTimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | import java.time.*; 4 | import java.time.format.DateTimeFormatter; 5 | 6 | import static java.time.OffsetDateTime.now; 7 | 8 | @SuppressWarnings("unused") 9 | public class DateTimeUtil { 10 | 11 | private static final DateTimeFormatter ISO_DATE_TIME_FORMAT = DateTimeFormatter 12 | .ofPattern("dd/MM/yyyy HH:mm:ss"); 13 | 14 | private static final DateTimeFormatter ISO_DATE_FORMAT_NO_TIME = DateTimeFormatter 15 | .ofPattern("dd/MM/yyyy"); 16 | 17 | private static final DateTimeFormatter ISO_DATE_FORMAT_LONG_NO_TIME = DateTimeFormatter 18 | .ofPattern("d MMM yyyy"); 19 | 20 | private static final DateTimeFormatter ISO_DATE_FORMAT_LONG_MILI = DateTimeFormatter 21 | .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 22 | 23 | private static final DateTimeFormatter ISO_DATE_FORMAT_SHORT_NO_TIME = DateTimeFormatter 24 | .ofPattern("yyyy-MM-dd"); 25 | 26 | public static OffsetDateTime getEffectiveDate() { 27 | return getOffsetDateTime(2018, 1, 1); 28 | } 29 | 30 | public static OffsetDateTime getExpirationDate() { 31 | return getOffsetDateTime(3000, 1, 1); 32 | } 33 | 34 | public static String getNowUnixTimestampDate() { 35 | return String.valueOf(System.currentTimeMillis() / 1000L); 36 | } 37 | 38 | public static OffsetDateTime getOffsetDateTime(int year, int month, int dayOfMonth) { 39 | return OffsetDateTime 40 | .of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(0, 0), ZoneOffset.UTC); 41 | } 42 | 43 | public static String localDateNow() { 44 | return OffsetDateTime.of(now().toLocalDateTime(), ZoneOffset.UTC) 45 | .format(ISO_DATE_FORMAT_NO_TIME); 46 | } 47 | 48 | public static OffsetDateTime getDateToday() { 49 | return OffsetDateTime.of(now().toLocalDateTime(), ZoneOffset.UTC); 50 | } 51 | 52 | public static String getLongDateStringFromDateString(String dateString) { 53 | LocalDate date = LocalDate.parse(dateString, ISO_DATE_FORMAT_NO_TIME); 54 | return ISO_DATE_FORMAT_LONG_NO_TIME.format(date); 55 | } 56 | 57 | public static String getShortDateStringFromDateString(String dateString) { 58 | LocalDate date = LocalDate.parse(dateString, ISO_DATE_FORMAT_NO_TIME); 59 | return ISO_DATE_FORMAT_SHORT_NO_TIME.format(date); 60 | } 61 | 62 | public static String getLongDateMiliString() { 63 | return ISO_DATE_FORMAT_LONG_MILI.format(getDateToday()); 64 | } 65 | 66 | public static OffsetDateTime localDateTimeNow() { 67 | return OffsetDateTime.of(now().toLocalDateTime(), ZoneOffset.UTC); 68 | } 69 | 70 | /** 71 | * @return - get month date as 01-2020 String 72 | */ 73 | public static String getMonthYearNumericalString() { 74 | return now().toString().split("-")[1] + "/" + now().getYear(); 75 | } 76 | 77 | /** 78 | * @return next week day as dd/MM/yyyy 79 | */ 80 | public static String getNextDayOfWeekNumericalFormat() { 81 | LocalDateTime date = LocalDateTime.now(); 82 | do { 83 | date = date.plusDays(1); 84 | } while (date.getDayOfWeek().getValue() >= 5); 85 | return date.format(ISO_DATE_FORMAT_NO_TIME); 86 | } 87 | 88 | public static String featureDateManager(String tableDate) { 89 | if (tableDate != null) { 90 | 91 | final String[] dateTableData = tableDate.split("\\s+"); 92 | String switchType; 93 | int dateValue = 0; 94 | 95 | if (dateTableData.length > 1) { 96 | switchType = dateTableData[0] + " " + dateTableData[1]; 97 | dateValue = Integer.parseInt(dateTableData[2]); 98 | } else { 99 | switchType = "Day"; 100 | } 101 | 102 | final LocalDateTime date = LocalDateTime.now(); 103 | 104 | switch (switchType) { 105 | case "Day +": 106 | return date.plusDays(dateValue).format(ISO_DATE_FORMAT_NO_TIME); 107 | case "Day -": 108 | return date.minusDays(dateValue).format(ISO_DATE_FORMAT_NO_TIME); 109 | case "Month +": 110 | return date.plusMonths(dateValue).format(ISO_DATE_FORMAT_NO_TIME); 111 | case "Month -": 112 | return date.minusMonths(dateValue).format(ISO_DATE_FORMAT_NO_TIME); 113 | case "Year +": 114 | return date.plusYears(dateValue).format(ISO_DATE_FORMAT_NO_TIME); 115 | case "Year -": 116 | return date.minusYears(dateValue).format(ISO_DATE_FORMAT_NO_TIME); 117 | default: 118 | return date.format(ISO_DATE_FORMAT_NO_TIME); 119 | } 120 | } 121 | return null; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/HookUtil.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | import io.cucumber.java.Scenario; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.nio.charset.StandardCharsets; 8 | 9 | @Service 10 | public class HookUtil { 11 | 12 | @Autowired 13 | LogManager logManager; 14 | 15 | public void endOfTest(Scenario scenario) { 16 | 17 | if (scenario.getStatus() != null) { 18 | if (scenario.isFailed()) { 19 | String filename = scenario.getName().replaceAll("\\s+", "_"); 20 | final String featureError = scenario.getId().replaceAll("\\s+", "_").replaceAll(":", "_").split("\\.")[1]; 21 | filename = filename + "_" + featureError; 22 | scenario.attach(filename.getBytes(StandardCharsets.UTF_8), "image/png", filename); 23 | } 24 | } 25 | 26 | logManager.info(""); 27 | logManager.info("=========================================================================="); 28 | logManager.info("================================Test " + scenario.getStatus().toString() + "==============================="); 29 | logManager.info("=========================================================================="); 30 | logManager.info(""); 31 | } 32 | 33 | 34 | } -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/LogManager.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.Logger; 5 | import ch.qos.logback.classic.LoggerContext; 6 | import ch.qos.logback.classic.PatternLayout; 7 | import ch.qos.logback.classic.encoder.PatternLayoutEncoder; 8 | import ch.qos.logback.classic.spi.ILoggingEvent; 9 | import ch.qos.logback.core.ConsoleAppender; 10 | import ch.qos.logback.core.FileAppender; 11 | import ch.qos.logback.core.filter.EvaluatorFilter; 12 | import ch.qos.logback.core.pattern.color.ANSIConstants; 13 | import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.stereotype.Service; 16 | 17 | @Service 18 | public class LogManager extends ForegroundCompositeConverterBase { 19 | 20 | @Override 21 | protected String getForegroundColorCode(ILoggingEvent event) { 22 | Level level = event.getLevel(); 23 | return switch (level.toInt()) { 24 | case Level.ERROR_INT -> ANSIConstants.BOLD + ANSIConstants.RED_FG; 25 | case Level.WARN_INT -> ANSIConstants.RED_FG; 26 | case Level.INFO_INT -> ANSIConstants.CYAN_FG; 27 | default -> ANSIConstants.DEFAULT_FG; 28 | }; 29 | } 30 | 31 | private final static ThreadLocal logFactory = new ThreadLocal<>(); 32 | 33 | public void createNewLogger(String className) { 34 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 35 | 36 | // Use the custom evaluator instead of JaninoEventEvaluator 37 | CustomEventEvaluator customEventEvaluator = new CustomEventEvaluator(); 38 | customEventEvaluator.setContext(loggerContext); 39 | 40 | EvaluatorFilter evaluatorFilter = new EvaluatorFilter<>(); 41 | evaluatorFilter.setEvaluator(customEventEvaluator); 42 | 43 | FileAppender fileAppender = fileAppender(className, loggerContext, evaluatorFilter); 44 | fileAppender.start(); 45 | 46 | ConsoleAppender consoleAppender = consoleAppender(loggerContext, evaluatorFilter); 47 | consoleAppender.start(); 48 | 49 | Logger logger = (Logger) LoggerFactory.getLogger(className); 50 | logger.addAppender(fileAppender); 51 | logger.addAppender(consoleAppender); 52 | 53 | logger.setLevel(Level.DEBUG); 54 | logger.setAdditive(false); 55 | logFactory.set(logger); 56 | } 57 | 58 | private FileAppender fileAppender(String className, LoggerContext loggerContext, EvaluatorFilter evaluatorFilter) { 59 | PatternLayout filePattern = new PatternLayout(); 60 | filePattern.setContext(loggerContext); 61 | filePattern.setPattern("[%d{ISO8601}] %-5level [%logger{100}]: %msg%n%throwable"); 62 | filePattern.start(); 63 | 64 | PatternLayoutEncoder encoder = new PatternLayoutEncoder(); 65 | encoder.setPattern(filePattern.getPattern()); 66 | encoder.setContext(loggerContext); 67 | encoder.start(); 68 | 69 | FileAppender fileAppender = new FileAppender<>(); 70 | fileAppender.setFile("logs/" + className + ".log"); 71 | fileAppender.setAppend(false); 72 | fileAppender.setEncoder(encoder); 73 | fileAppender.setContext(loggerContext); 74 | fileAppender.addFilter(evaluatorFilter); 75 | 76 | return fileAppender; 77 | } 78 | 79 | private ConsoleAppender consoleAppender(LoggerContext loggerContext, EvaluatorFilter evaluatorFilter) { 80 | PatternLayout consolePattern = new PatternLayout(); 81 | consolePattern.setContext(loggerContext); 82 | consolePattern.setPattern("%blue([%d{ISO8601}]) %highlight(%colourPicker(%-5level)) %blue([%logger{100}]:) %colourPicker(%msg%n%throwable)"); 83 | consolePattern.start(); 84 | 85 | PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder(); 86 | consoleEncoder.setPattern(consolePattern.getPattern()); 87 | consoleEncoder.setContext(loggerContext); 88 | consoleEncoder.start(); 89 | 90 | ConsoleAppender consoleAppender = new ConsoleAppender<>(); 91 | consoleAppender.setEncoder(consoleEncoder); 92 | consoleAppender.setContext(loggerContext); 93 | consoleAppender.addFilter(evaluatorFilter); 94 | 95 | return consoleAppender; 96 | } 97 | 98 | private static Logger getLogger() { 99 | return logFactory.get(); 100 | } 101 | 102 | public void info(String value) { 103 | getLogger().info(value); 104 | } 105 | 106 | public void debug(String value) { 107 | getLogger().debug(value); 108 | } 109 | 110 | public void warn(String value) { 111 | getLogger().warn(value); 112 | } 113 | 114 | public void error(String value) { 115 | getLogger().error(value); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /common/src/main/java/com/cmccarthy/common/utils/StringUtil.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.common.utils; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.Random; 5 | import java.util.concurrent.ThreadLocalRandom; 6 | 7 | import static org.apache.commons.lang3.RandomStringUtils.random; 8 | 9 | @SuppressWarnings("unused") 10 | public class StringUtil { 11 | 12 | private static final Random random = new Random(); 13 | 14 | public static boolean getRandomBoolean() { 15 | return random.nextBoolean(); 16 | } 17 | 18 | public static int getRandomNumber(int min, int max) { 19 | if (min > max) { 20 | throw new IllegalArgumentException( 21 | "Min (" + min + ") should not be larger than Max (" + max + ")"); 22 | } 23 | return ThreadLocalRandom.current().nextInt(min, max + 1); 24 | } 25 | 26 | public static String getRandomAlphaString(int length) { 27 | return random(length, true, false); 28 | } 29 | 30 | public static String getRandomAlphaString(int min, int max) { 31 | return random(getRandomNumber(min, max), true, false); 32 | } 33 | 34 | public static String getRandomNumericString(int length, int min, int max) { 35 | return random(length, min, max, false, true); 36 | } 37 | 38 | public static String getRandomNumericString(int min, int max) { 39 | return random(getRandomNumber(min, max), false, true); 40 | } 41 | 42 | public static String getRandomAlphaNumericString(int length) { 43 | return random(length, true, true); 44 | } 45 | 46 | public static String getRandomAlphaNumericString(int min, int max) { 47 | return random(getRandomNumber(min, max), true, true); 48 | } 49 | 50 | public static String getRandomAmount(String min, String max) { 51 | if (Double.parseDouble(min) > Double.parseDouble(max)) { 52 | throw new IllegalArgumentException( 53 | ": Min (" + min + ") should not be larger than Max (" + max + ")"); 54 | } 55 | return String.format("%.2f", ThreadLocalRandom.current() 56 | .nextDouble(Double.parseDouble(min), Double.parseDouble(max) + 0.01)); 57 | } 58 | 59 | public static double getRandomAmount(double min, double max) { 60 | if (min > max) { 61 | throw new IllegalArgumentException( 62 | ": Min (" + min + ") should not be larger than Max (" + max + ")"); 63 | } 64 | return Double.parseDouble( 65 | String.format("%.2f", ThreadLocalRandom.current().nextDouble(min, max + 0.01))); 66 | } 67 | 68 | public static String featureStringManager(String tableValue) { 69 | if (tableValue != null) { 70 | final String[] dateTableData = tableValue.split("\\s+"); 71 | 72 | final String switchType = dateTableData[0]; 73 | 74 | final int length = Integer.parseInt(dateTableData[1]); 75 | 76 | switch (switchType) { 77 | case "StringInteger": 78 | return getRandomAlphaNumericString(length); 79 | case "String": 80 | return getRandomAlphaString(length); 81 | case "Integer": 82 | return getRandomNumericString(length, 2, 66); 83 | default: 84 | throw new NoSuchElementException("Could not create a String of type : " + switchType); 85 | } 86 | } else { 87 | return null; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /common/src/main/resources/allure.properties: -------------------------------------------------------------------------------- 1 | allure.results.directory=target/allure-results 2 | allure.link.issue.pattern=https://example.org/issue/{} 3 | allure.link.tms.pattern=https://example.org/tms/{} -------------------------------------------------------------------------------- /common/src/main/resources/application-cloud-provider.properties: -------------------------------------------------------------------------------- 1 | weather.url.value=http://api.openweathermap.org/data/2.5/weather 2 | wikipedia.url.value=https://www.wikipedia.org/ 3 | browser=chrome 4 | gridUrl=http://selenium_hub:4444/wd/hub 5 | -------------------------------------------------------------------------------- /common/src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | weather.url.value=http://api.openweathermap.org/data/2.5/weather 2 | wikipedia.url.value=https://www.wikipedia.org/ 3 | browser=chrome -------------------------------------------------------------------------------- /common/src/main/resources/application-headless-github.properties: -------------------------------------------------------------------------------- 1 | weather.url.value=http://api.openweathermap.org/data/2.5/weather 2 | wikipedia.url.value=https://www.wikipedia.org/ 3 | browser=chrome 4 | github=true 5 | -------------------------------------------------------------------------------- /common/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | weather.url.value=http://api.openweathermap.org/data/2.5/weather 2 | wikipedia.url.value=https://www.wikipedia.org/ 3 | browser=chrome -------------------------------------------------------------------------------- /common/src/main/resources/application-uat.properties: -------------------------------------------------------------------------------- 1 | weather.url.value=http://api.openweathermap.org/data/2.5/weather 2 | wikipedia.url.value=https://www.wikipedia.org/ 3 | browser=opera -------------------------------------------------------------------------------- /common/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=@activeProfile@ -------------------------------------------------------------------------------- /common/src/main/resources/demo/allure-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/9656b0e42f8ea2221f3185d3086d49cfe2fad2ad/common/src/main/resources/demo/allure-report.png -------------------------------------------------------------------------------- /common/src/main/resources/demo/extent-report.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/9656b0e42f8ea2221f3185d3086d49cfe2fad2ad/common/src/main/resources/demo/extent-report.jpg -------------------------------------------------------------------------------- /common/src/main/resources/downloadDriver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | export SCRIPT_DIR=../../../../wikipedia/src/test/resources 5 | cd $SCRIPT_DIR && 6 | 7 | ROOT_DIR=$(pwd) 8 | TEST_RESOURCES=$ROOT_DIR 9 | FILE_EXTENSION="" 10 | 11 | echo $TEST_RESOURCES 12 | 13 | case "$OSTYPE" in 14 | solaris*) echo "$OSTYPE not supported" ;; 15 | darwin*) OS='mac64' ;; 16 | linux*) OS='linux64' ;; 17 | bsd*) echo "$OSTYPE not supported" ;; 18 | msys*) OS='win32' FILE_EXTENSION=".exe" ;; 19 | *) echo "unknown $OSTYPE" ;; 20 | esac 21 | 22 | if [ ! -f "$TEST_RESOURCES/drivers/chromedriver"$FILE_EXTENSION ]; then 23 | cd "$TEST_RESOURCES/drivers" && 24 | mkdir "temp" && 25 | cd "temp" && 26 | curl -L -k --output driver.zip https://www.nuget.org/api/v2/package/Selenium.WebDriver.ChromeDriver/ --ssl-no-revoke && 27 | unzip driver.zip && 28 | cd driver/$OS && 29 | cp "chromedriver$FILE_EXTENSION" "$TEST_RESOURCES/drivers" && 30 | chmod +700 "$TEST_RESOURCES/drivers/chromedriver"$FILE_EXTENSION && 31 | cd ../../../ && 32 | rm -rf temp && 33 | cd $ROOT_DIR 34 | fi 35 | 36 | if [ ! -f "$TEST_RESOURCES/drivers/geckodriver"$FILE_EXTENSION ]; then 37 | cd "$TEST_RESOURCES/drivers" && 38 | mkdir "temp" && 39 | cd "temp" && 40 | curl -L -k --output driver.zip https://www.nuget.org/api/v2/package/Selenium.WebDriver.GeckoDriver/ --ssl-no-revoke && 41 | unzip driver.zip && 42 | cd driver/$OS && 43 | cp "geckodriver$FILE_EXTENSION" "$TEST_RESOURCES/drivers" && 44 | chmod +700 "$TEST_RESOURCES/drivers/geckodriver"$FILE_EXTENSION && 45 | cd ../../../ && 46 | rm -rf temp && 47 | cd $ROOT_DIR 48 | fi 49 | 50 | #uncomment to keep bash open 51 | #! /bin/bash 52 | -------------------------------------------------------------------------------- /common/src/main/resources/extent.properties: -------------------------------------------------------------------------------- 1 | extent.reporter.spark.start=true 2 | extent.reporter.spark.config=../common/src/main/resources/extent.xml 3 | extent.reporter.spark.out=target/cucumber/report.html 4 | screenshot.dir=target/ 5 | screenshot.rel.path=./ -------------------------------------------------------------------------------- /common/src/main/resources/extent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Framework 7 | 8 | 9 | 10 | UTF-8 11 | 12 | 13 | 14 | http 15 | 16 | 17 | Test Framework 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | dark 37 | 38 | 39 | MMM dd, yyyy HH:mm:ss 40 | 41 | 42 | -------------------------------------------------------------------------------- /common/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-cucumber-testng-parallel-test-harness 8 | spring-cucumber-testng-parallel-test-harness 9 | pom 10 | 1.0-SNAPSHOT 11 | 12 | 13 | 14 | cmccarthy 15 | cmccarthy 16 | https://github.com/cmccarthyIrl 17 | 18 | 19 | 20 | 21 | 22 | MIT License 23 | http://www.opensource.org/licenses/mit-license.php 24 | repo 25 | 26 | 27 | 28 | 29 | common 30 | weather 31 | wikipedia 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-parent 37 | 3.5.0 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-dependencies 46 | 3.5.0 47 | pom 48 | import 49 | 50 | 51 | 52 | 53 | 54 | headless-github 55 | 17 56 | UTF-8 57 | UTF-8 58 | 20250517 59 | 5.5.0 60 | 7.22.0 61 | 7.22.0 62 | 7.22.0 63 | 1.5.13 64 | 32.1.1 65 | 2.15.1 66 | 3.23.1 67 | 1.14.0 68 | 2.29.1 69 | 4.33.0 70 | 1.7.0 71 | 3.1.2 72 | 3.2.3 73 | 74 | 75 | 76 | 77 | org.json 78 | json 79 | ${json.version} 80 | 81 | 82 | 83 | 84 | io.rest-assured 85 | rest-assured 86 | ${rest-assured.version} 87 | 88 | 89 | io.cucumber 90 | cucumber-java 91 | ${cucumber-java.version} 92 | 93 | 94 | io.cucumber 95 | cucumber-spring 96 | ${cucumber-spring.version} 97 | 98 | 99 | io.cucumber 100 | cucumber-testng 101 | ${cucumber-testng.version} 102 | 103 | 104 | io.cucumber 105 | gherkin 106 | ${gherkin.version} 107 | 108 | 109 | 110 | 111 | commons-io 112 | commons-io 113 | ${commons-io.version} 114 | 115 | 116 | org.assertj 117 | assertj-core 118 | ${assertj-core.version} 119 | 120 | 121 | tech.grasshopper 122 | extentreports-cucumber7-adapter 123 | ${extentreports-cucumber7-adapter.version} 124 | 125 | 126 | io.qameta.allure 127 | allure-cucumber7-jvm 128 | ${allure-cucumber7-jvm.version} 129 | 130 | 131 | ch.qos.logback 132 | logback-classic 133 | ${logback.version} 134 | 135 | 136 | ch.qos.logback 137 | logback-core 138 | ${logback.version} 139 | 140 | 141 | 142 | 143 | org.springframework.amqp 144 | spring-rabbit 145 | 146 | 147 | org.springframework.boot 148 | spring-boot-starter-web 149 | 150 | 151 | org.springframework.boot 152 | spring-boot-starter-test 153 | test 154 | 155 | 156 | com.vaadin.external.google 157 | android-json 158 | 159 | 160 | logback-classic 161 | ch.qos.logback 162 | 163 | 164 | 165 | 166 | org.springframework 167 | spring-test 168 | 169 | 170 | org.springframework.boot 171 | spring-boot-starter-aop 172 | 173 | 174 | 175 | 176 | net.sf.jtidy 177 | jtidy 178 | r938 179 | 180 | 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-compiler-plugin 187 | ${maven-compiler-plugin.version} 188 | 189 | ${java.version} 190 | ${java.version} 191 | 192 | 193 | 194 | org.apache.maven.plugins 195 | maven-jar-plugin 196 | 197 | 198 | 199 | test-jar 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | dev 212 | 213 | dev 214 | 215 | 216 | 217 | prod 218 | 219 | prod 220 | 221 | 222 | 223 | uat 224 | 225 | uat 226 | 227 | 228 | 229 | cloud-provider 230 | 231 | cloud-provider 232 | 233 | 234 | 235 | headless-github 236 | 237 | headless-github 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /weather/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cucumber-testng-parallel-test-harness 7 | spring-cucumber-testng-parallel-test-harness 8 | 1.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | 13 | weather 14 | 15 | 16 | 17 | spring-cucumber-testng-parallel-test-harness 18 | common 19 | 1.0-SNAPSHOT 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.apache.maven.plugins 27 | maven-surefire-plugin 28 | ${maven.surefire.plugin.version} 29 | 30 | 31 | src/test/resources/suite/WeatherSuiteTest.xml 32 | 33 | 34 | 35 | 36 | org.apache.maven.surefire 37 | surefire-testng 38 | ${surefire.testng.version} 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/WeatherRunnerTest.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api; 2 | 3 | import io.cucumber.testng.AbstractTestNGCucumberTests; 4 | import io.cucumber.testng.CucumberOptions; 5 | import org.testng.annotations.DataProvider; 6 | 7 | @CucumberOptions( 8 | features = { 9 | "src/test/resources/feature/WeatherTest.feature" 10 | }, 11 | plugin = { 12 | "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:", 13 | "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm" 14 | }) 15 | public class WeatherRunnerTest extends AbstractTestNGCucumberTests { 16 | @Override 17 | @DataProvider(parallel = true) 18 | public Object[][] scenarios() { 19 | return super.scenarios(); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/config/WeatherAbstractTestDefinition.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.config; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | import org.springframework.test.context.ContextConfiguration; 5 | 6 | @ContextConfiguration(classes = {WeatherContextConfiguration.class}) 7 | @SpringBootTest 8 | public class WeatherAbstractTestDefinition { 9 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/config/WeatherContextConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.config; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.retry.annotation.EnableRetry; 7 | 8 | @EnableRetry 9 | @Configuration 10 | @ComponentScan({ 11 | "com.cmccarthy.api", 12 | "com.cmccarthy.common" 13 | }) 14 | @PropertySource("classpath:/application.properties") 15 | public class WeatherContextConfiguration { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/Clouds.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | @JsonInclude(JsonInclude.Include.NON_NULL) 8 | @JsonPropertyOrder({ 9 | "all" 10 | }) 11 | public class Clouds { 12 | 13 | @JsonProperty("all") 14 | private Integer all; 15 | 16 | @JsonProperty("all") 17 | public Integer getAll() { 18 | return all; 19 | } 20 | 21 | @JsonProperty("all") 22 | public void setAll(Integer all) { 23 | this.all = all; 24 | } 25 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/Coord.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | @JsonPropertyOrder({ 10 | "lon", 11 | "lat" 12 | }) 13 | public class Coord { 14 | 15 | @JsonProperty("lon") 16 | private Double lon; 17 | @JsonProperty("lat") 18 | private Double lat; 19 | @JsonIgnore 20 | private Map additionalProperties = new HashMap(); 21 | 22 | @JsonProperty("lon") 23 | public Double getLon() { 24 | return lon; 25 | } 26 | 27 | @JsonProperty("lon") 28 | public void setLon(Double lon) { 29 | this.lon = lon; 30 | } 31 | 32 | @JsonProperty("lat") 33 | public Double getLat() { 34 | return lat; 35 | } 36 | 37 | @JsonProperty("lat") 38 | public void setLat(Double lat) { 39 | this.lat = lat; 40 | } 41 | 42 | @JsonAnyGetter 43 | public Map getAdditionalProperties() { 44 | return this.additionalProperties; 45 | } 46 | 47 | @JsonAnySetter 48 | public void setAdditionalProperty(String name, Object value) { 49 | this.additionalProperties.put(name, value); 50 | } 51 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/LocationWeatherRootResponse.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public class LocationWeatherRootResponse { 11 | 12 | private Coord coord; 13 | private List weather = null; 14 | private String base; 15 | private Main main; 16 | private Integer visibility; 17 | private Wind wind; 18 | private Clouds clouds; 19 | private Integer dt; 20 | private Sys sys; 21 | private Integer timezone; 22 | private Integer id; 23 | private String name; 24 | private Integer cod; 25 | private Map additionalProperties = new HashMap(); 26 | 27 | public Coord getCoord() { 28 | return coord; 29 | } 30 | 31 | public void setCoord(Coord coord) { 32 | this.coord = coord; 33 | } 34 | 35 | public List getWeather() { 36 | return weather; 37 | } 38 | 39 | public void setWeather(List weather) { 40 | this.weather = weather; 41 | } 42 | 43 | public String getBase() { 44 | return base; 45 | } 46 | 47 | public void setBase(String base) { 48 | this.base = base; 49 | } 50 | 51 | public Main getMain() { 52 | return main; 53 | } 54 | 55 | public void setMain(Main main) { 56 | this.main = main; 57 | } 58 | 59 | public Integer getVisibility() { 60 | return visibility; 61 | } 62 | 63 | public void setVisibility(Integer visibility) { 64 | this.visibility = visibility; 65 | } 66 | 67 | public Wind getWind() { 68 | return wind; 69 | } 70 | 71 | public void setWind(Wind wind) { 72 | this.wind = wind; 73 | } 74 | 75 | public Clouds getClouds() { 76 | return clouds; 77 | } 78 | 79 | public void setClouds(Clouds clouds) { 80 | this.clouds = clouds; 81 | } 82 | 83 | public Integer getDt() { 84 | return dt; 85 | } 86 | 87 | public void setDt(Integer dt) { 88 | this.dt = dt; 89 | } 90 | 91 | public Sys getSys() { 92 | return sys; 93 | } 94 | 95 | public void setSys(Sys sys) { 96 | this.sys = sys; 97 | } 98 | 99 | public Integer getTimezone() { 100 | return timezone; 101 | } 102 | 103 | public void setTimezone(Integer timezone) { 104 | this.timezone = timezone; 105 | } 106 | 107 | public Integer getId() { 108 | return id; 109 | } 110 | 111 | public void setId(Integer id) { 112 | this.id = id; 113 | } 114 | 115 | public String getName() { 116 | return name; 117 | } 118 | 119 | public void setName(String name) { 120 | this.name = name; 121 | } 122 | 123 | public Integer getCod() { 124 | return cod; 125 | } 126 | 127 | public void setCod(Integer cod) { 128 | this.cod = cod; 129 | } 130 | 131 | public Map getAdditionalProperties() { 132 | return this.additionalProperties; 133 | } 134 | 135 | public void setAdditionalProperty(String name, Object value) { 136 | this.additionalProperties.put(name, value); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/Main.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | @JsonPropertyOrder({ 10 | "temp", 11 | "feels_like", 12 | "temp_min", 13 | "temp_max", 14 | "pressure", 15 | "humidity" 16 | }) 17 | public class Main { 18 | 19 | @JsonProperty("temp") 20 | private Double temp; 21 | @JsonProperty("feels_like") 22 | private Double feelsLike; 23 | @JsonProperty("temp_min") 24 | private Double tempMin; 25 | @JsonProperty("temp_max") 26 | private Double tempMax; 27 | @JsonProperty("pressure") 28 | private Integer pressure; 29 | @JsonProperty("humidity") 30 | private Integer humidity; 31 | @JsonIgnore 32 | private Map additionalProperties = new HashMap(); 33 | 34 | @JsonProperty("temp") 35 | public Double getTemp() { 36 | return temp; 37 | } 38 | 39 | @JsonProperty("temp") 40 | public void setTemp(Double temp) { 41 | this.temp = temp; 42 | } 43 | 44 | @JsonProperty("feels_like") 45 | public Double getFeelsLike() { 46 | return feelsLike; 47 | } 48 | 49 | @JsonProperty("feels_like") 50 | public void setFeelsLike(Double feelsLike) { 51 | this.feelsLike = feelsLike; 52 | } 53 | 54 | @JsonProperty("temp_min") 55 | public Double getTempMin() { 56 | return tempMin; 57 | } 58 | 59 | @JsonProperty("temp_min") 60 | public void setTempMin(Double tempMin) { 61 | this.tempMin = tempMin; 62 | } 63 | 64 | @JsonProperty("temp_max") 65 | public Double getTempMax() { 66 | return tempMax; 67 | } 68 | 69 | @JsonProperty("temp_max") 70 | public void setTempMax(Double tempMax) { 71 | this.tempMax = tempMax; 72 | } 73 | 74 | @JsonProperty("pressure") 75 | public Integer getPressure() { 76 | return pressure; 77 | } 78 | 79 | @JsonProperty("pressure") 80 | public void setPressure(Integer pressure) { 81 | this.pressure = pressure; 82 | } 83 | 84 | @JsonProperty("humidity") 85 | public Integer getHumidity() { 86 | return humidity; 87 | } 88 | 89 | @JsonProperty("humidity") 90 | public void setHumidity(Integer humidity) { 91 | this.humidity = humidity; 92 | } 93 | 94 | @JsonAnyGetter 95 | public Map getAdditionalProperties() { 96 | return this.additionalProperties; 97 | } 98 | 99 | @JsonAnySetter 100 | public void setAdditionalProperty(String name, Object value) { 101 | this.additionalProperties.put(name, value); 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/Sys.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | @JsonPropertyOrder({ 10 | "type", 11 | "id", 12 | "country", 13 | "sunrise", 14 | "sunset" 15 | }) 16 | public class Sys { 17 | 18 | @JsonProperty("type") 19 | private Integer type; 20 | @JsonProperty("id") 21 | private Integer id; 22 | @JsonProperty("country") 23 | private String country; 24 | @JsonProperty("sunrise") 25 | private Integer sunrise; 26 | @JsonProperty("sunset") 27 | private Integer sunset; 28 | @JsonIgnore 29 | private Map additionalProperties = new HashMap(); 30 | 31 | @JsonProperty("type") 32 | public Integer getType() { 33 | return type; 34 | } 35 | 36 | @JsonProperty("type") 37 | public void setType(Integer type) { 38 | this.type = type; 39 | } 40 | 41 | @JsonProperty("id") 42 | public Integer getId() { 43 | return id; 44 | } 45 | 46 | @JsonProperty("id") 47 | public void setId(Integer id) { 48 | this.id = id; 49 | } 50 | 51 | @JsonProperty("country") 52 | public String getCountry() { 53 | return country; 54 | } 55 | 56 | @JsonProperty("country") 57 | public void setCountry(String country) { 58 | this.country = country; 59 | } 60 | 61 | @JsonProperty("sunrise") 62 | public Integer getSunrise() { 63 | return sunrise; 64 | } 65 | 66 | @JsonProperty("sunrise") 67 | public void setSunrise(Integer sunrise) { 68 | this.sunrise = sunrise; 69 | } 70 | 71 | @JsonProperty("sunset") 72 | public Integer getSunset() { 73 | return sunset; 74 | } 75 | 76 | @JsonProperty("sunset") 77 | public void setSunset(Integer sunset) { 78 | this.sunset = sunset; 79 | } 80 | 81 | @JsonAnyGetter 82 | public Map getAdditionalProperties() { 83 | return this.additionalProperties; 84 | } 85 | 86 | @JsonAnySetter 87 | public void setAdditionalProperty(String name, Object value) { 88 | this.additionalProperties.put(name, value); 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/Weather.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | @JsonPropertyOrder({ 10 | "id", 11 | "main", 12 | "description", 13 | "icon" 14 | }) 15 | public class Weather { 16 | 17 | @JsonProperty("id") 18 | private Integer id; 19 | @JsonProperty("main") 20 | private String main; 21 | @JsonProperty("description") 22 | private String description; 23 | @JsonProperty("icon") 24 | private String icon; 25 | @JsonIgnore 26 | private Map additionalProperties = new HashMap(); 27 | 28 | @JsonProperty("id") 29 | public Integer getId() { 30 | return id; 31 | } 32 | 33 | @JsonProperty("id") 34 | public void setId(Integer id) { 35 | this.id = id; 36 | } 37 | 38 | @JsonProperty("main") 39 | public String getMain() { 40 | return main; 41 | } 42 | 43 | @JsonProperty("main") 44 | public void setMain(String main) { 45 | this.main = main; 46 | } 47 | 48 | @JsonProperty("description") 49 | public String getDescription() { 50 | return description; 51 | } 52 | 53 | @JsonProperty("description") 54 | public void setDescription(String description) { 55 | this.description = description; 56 | } 57 | 58 | @JsonProperty("icon") 59 | public String getIcon() { 60 | return icon; 61 | } 62 | 63 | @JsonProperty("icon") 64 | public void setIcon(String icon) { 65 | this.icon = icon; 66 | } 67 | 68 | @JsonAnyGetter 69 | public Map getAdditionalProperties() { 70 | return this.additionalProperties; 71 | } 72 | 73 | @JsonAnySetter 74 | public void setAdditionalProperty(String name, Object value) { 75 | this.additionalProperties.put(name, value); 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/model/response/Wind.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | @JsonPropertyOrder({ 10 | "speed", 11 | "deg" 12 | }) 13 | public class Wind { 14 | 15 | @JsonProperty("speed") 16 | private Double speed; 17 | @JsonProperty("deg") 18 | private Integer deg; 19 | @JsonIgnore 20 | private Map additionalProperties = new HashMap(); 21 | 22 | @JsonProperty("speed") 23 | public Double getSpeed() { 24 | return speed; 25 | } 26 | 27 | @JsonProperty("speed") 28 | public void setSpeed(Double speed) { 29 | this.speed = speed; 30 | } 31 | 32 | @JsonProperty("deg") 33 | public Integer getDeg() { 34 | return deg; 35 | } 36 | 37 | @JsonProperty("deg") 38 | public void setDeg(Integer deg) { 39 | this.deg = deg; 40 | } 41 | 42 | @JsonAnyGetter 43 | public Map getAdditionalProperties() { 44 | return this.additionalProperties; 45 | } 46 | 47 | @JsonAnySetter 48 | public void setAdditionalProperty(String name, Object value) { 49 | this.additionalProperties.put(name, value); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/service/WeatherService.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.service; 2 | 3 | import com.cmccarthy.common.service.RestService; 4 | import com.cmccarthy.common.service.StepDefinitionDataManager; 5 | import com.cmccarthy.common.utils.ApplicationProperties; 6 | import com.cmccarthy.common.utils.LogManager; 7 | import io.restassured.response.Response; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.net.HttpURLConnection; 12 | import java.util.NoSuchElementException; 13 | 14 | @Service 15 | public class WeatherService { 16 | @Autowired 17 | private RestService restService; 18 | @Autowired 19 | private LogManager logManager; 20 | @Autowired 21 | private StepDefinitionDataManager stepDefinitionDataManager; 22 | @Autowired 23 | private ApplicationProperties applicationProperties; 24 | 25 | public void getWeatherForLocation(String location) { 26 | Response response = restService.getRequestSpecification() 27 | .param("q", location) 28 | .param("appid", "0a1b11f110d4b6cd43181d23d724cb94") 29 | .get(applicationProperties.getWeatherAppUrl()); 30 | 31 | stepDefinitionDataManager.addToStoredObjectMap("class", response); 32 | 33 | if (response.statusCode() != HttpURLConnection.HTTP_OK) { 34 | logManager.info("Could not retrieve the weather forecast from the Response"); 35 | throw new NoSuchElementException("Could not retrieve the weather forecast from the Response"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/step/Hooks.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.step; 2 | 3 | 4 | import com.cmccarthy.api.config.WeatherAbstractTestDefinition; 5 | import com.cmccarthy.common.utils.HookUtil; 6 | import com.cmccarthy.common.utils.LogManager; 7 | import io.cucumber.java.After; 8 | import io.cucumber.java.Before; 9 | import io.cucumber.java.Scenario; 10 | import io.cucumber.spring.CucumberContextConfiguration; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | @CucumberContextConfiguration 14 | public class Hooks extends WeatherAbstractTestDefinition { 15 | @Autowired 16 | private LogManager logManager; 17 | @Autowired 18 | private HookUtil hookUtil; 19 | 20 | @Before 21 | public void beforeScenario(Scenario scenario) { 22 | String filename = scenario.getName().replaceAll("\\s+", "_"); 23 | logManager.createNewLogger(filename); 24 | } 25 | 26 | @After 27 | public void afterScenario(Scenario scenario) { 28 | hookUtil.endOfTest(scenario); 29 | } 30 | } -------------------------------------------------------------------------------- /weather/src/test/java/com/cmccarthy/api/step/WeatherStep.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.api.step; 2 | 3 | import com.cmccarthy.api.config.WeatherAbstractTestDefinition; 4 | import com.cmccarthy.api.model.response.LocationWeatherRootResponse; 5 | import com.cmccarthy.api.service.WeatherService; 6 | import com.cmccarthy.common.service.StepDefinitionDataManager; 7 | import com.cmccarthy.common.utils.LogManager; 8 | import com.google.gson.Gson; 9 | import io.cucumber.java.en.Given; 10 | import io.cucumber.java.en.Then; 11 | import io.restassured.response.Response; 12 | import org.assertj.core.api.SoftAssertions; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | 15 | public class WeatherStep extends WeatherAbstractTestDefinition { 16 | @Autowired 17 | private LogManager logManager; 18 | @Autowired 19 | private WeatherService weatherService; 20 | @Autowired 21 | private StepDefinitionDataManager stepDefinitionDataManager; 22 | 23 | @Then("^The weather for (.*) should be returned$") 24 | public void theWeatherForDublinShouldBeReturned(String location) { 25 | final SoftAssertions softAssertions = new SoftAssertions(); 26 | final LocationWeatherRootResponse locationWeatherRootResponse = 27 | new Gson().fromJson(((Response) stepDefinitionDataManager.getStoredObjectMap().get("class")).getBody().asString(), 28 | LocationWeatherRootResponse.class); 29 | logManager.info( 30 | "Verifying the Response location : " + locationWeatherRootResponse.getName() + ", is equal to the expected location : " + location); 31 | softAssertions.assertThat(locationWeatherRootResponse.getName()).as("Expected the weather forecast to be for : " + location) 32 | .withFailMessage("But it was for : " + locationWeatherRootResponse.getName()).isEqualToIgnoringCase(location); 33 | softAssertions.assertAll(); 34 | } 35 | 36 | @Given("^The user has requested the weather for (.*)$") 37 | public void theUserHasRequestedTheWeatherForDublin(String location) { 38 | logManager.info("The user makes an request for the weather in : " + location); 39 | weatherService.getWeatherForLocation(location); 40 | } 41 | } -------------------------------------------------------------------------------- /weather/src/test/resources/cucumber.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true -------------------------------------------------------------------------------- /weather/src/test/resources/feature/WeatherTest.feature: -------------------------------------------------------------------------------- 1 | Feature: Open Weather App - Random Tests 2 | 3 | Scenario: Verify the user can retrieve the weather for Sydney 4 | Given The user has requested the weather for Sydney 5 | Then The weather for Sydney should be returned 6 | 7 | Scenario: Verify the user can retrieve the weather for Dublin 8 | Given The user has requested the weather for Dublin 9 | Then The weather for Dublin should be returned -------------------------------------------------------------------------------- /weather/src/test/resources/feature/WeatherTest2.feature: -------------------------------------------------------------------------------- 1 | Feature: Open Weather App - Random Tests 2 2 | 3 | Scenario: Verify the user can retrieve the weather for Dublin 2 4 | Given The user has requested the weather for Dublin 5 | Then The weather for Dublin should be returned 6 | -------------------------------------------------------------------------------- /weather/src/test/resources/suite/WeatherSuiteTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /wikipedia/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cucumber-testng-parallel-test-harness 7 | spring-cucumber-testng-parallel-test-harness 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | wikipedia 12 | 13 | 14 | 15 | spring-cucumber-testng-parallel-test-harness 16 | common 17 | 1.0-SNAPSHOT 18 | 19 | 20 | org.seleniumhq.selenium 21 | selenium-java 22 | ${selenium-java.version} 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-surefire-plugin 31 | ${maven.surefire.plugin.version} 32 | 33 | 34 | src/test/resources/suite/WikipediaSuiteTest.xml 35 | 36 | 37 | 38 | 39 | org.apache.maven.surefire 40 | surefire-testng 41 | ${surefire.testng.version} 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/WikipediaParallelRunnerTest.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui; 2 | 3 | import io.cucumber.testng.AbstractTestNGCucumberTests; 4 | import io.cucumber.testng.CucumberOptions; 5 | import org.testng.annotations.DataProvider; 6 | 7 | @CucumberOptions( 8 | features = { 9 | "src/test/resources/feature/" 10 | }, 11 | plugin = { 12 | "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:", 13 | "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm" 14 | }) 15 | public class WikipediaParallelRunnerTest extends AbstractTestNGCucumberTests { 16 | 17 | @Override 18 | @DataProvider(parallel = true) 19 | public Object[][] scenarios() { 20 | return super.scenarios(); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/annotations/PageObject.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.annotations; 2 | 3 | import org.openqa.selenium.support.FindBy; 4 | import org.openqa.selenium.support.PageFactoryFinder; 5 | import org.springframework.context.annotation.Lazy; 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.Target; 12 | 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | @Retention(RUNTIME) 16 | @Target({ElementType.FIELD, ElementType.TYPE}) 17 | @PageFactoryFinder(FindBy.FindByBuilder.class) 18 | @Lazy 19 | @Component 20 | @Scope("prototype") 21 | public @interface PageObject { 22 | } 23 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaAbstractTestDefinition.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.config; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | import org.springframework.test.context.ContextConfiguration; 5 | 6 | @ContextConfiguration(classes = {WikipediaContextConfiguration.class}) 7 | @SpringBootTest 8 | public class WikipediaAbstractTestDefinition { 9 | } -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.config; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.retry.annotation.EnableRetry; 7 | 8 | @EnableRetry 9 | @Configuration 10 | @ComponentScan({"com.cmccarthy.ui", "com.cmccarthy.common"}) 11 | @PropertySource("classpath:/application.properties") 12 | public class WikipediaContextConfiguration { 13 | } 14 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/page/AbstractPage.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.page; 2 | 3 | import com.cmccarthy.ui.utils.DriverManager; 4 | import org.openqa.selenium.support.PageFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public abstract class AbstractPage { 10 | 11 | @Autowired 12 | private DriverManager driverManager; 13 | 14 | protected AbstractPage(DriverManager driverManager) { 15 | PageFactory.initElements(driverManager.getDriver(), this); 16 | } 17 | 18 | protected void openAt(String url) { 19 | driverManager.getDriver().get(url); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/page/WikipediaCommonPage.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.page; 2 | 3 | import com.cmccarthy.ui.annotations.PageObject; 4 | import com.cmccarthy.ui.utils.DriverManager; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.support.FindBy; 7 | import org.openqa.selenium.support.How; 8 | 9 | @PageObject 10 | public class WikipediaCommonPage extends AbstractPage { 11 | 12 | @FindBy(how = How.CLASS_NAME, using = "mainpage-welcome-sitename") 13 | private WebElement centralLogo; 14 | 15 | public WikipediaCommonPage(DriverManager driverManager) { 16 | super(driverManager); 17 | } 18 | 19 | public WebElement getCentralLogo() { 20 | return centralLogo; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/page/WikipediaHomePage.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.page; 2 | 3 | import com.cmccarthy.ui.annotations.PageObject; 4 | import com.cmccarthy.ui.utils.DriverManager; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.support.FindBy; 7 | import org.openqa.selenium.support.How; 8 | 9 | @PageObject 10 | public class WikipediaHomePage extends AbstractPage { 11 | 12 | @FindBy(how = How.XPATH, using = "//a[contains(@href,'commons.wikimedia.org')]") 13 | private WebElement COMMONS_LOGO; 14 | 15 | @FindBy(how = How.CLASS_NAME, using = "central-featured-logo") 16 | private WebElement CENTRAL_LOGO; 17 | 18 | public WikipediaHomePage(DriverManager driverManager) { 19 | super(driverManager); 20 | } 21 | 22 | public void open(String url) { 23 | openAt(url); 24 | } 25 | 26 | public WebElement getCommonPage() { 27 | return COMMONS_LOGO; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/step/AbstractStep.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.step; 2 | 3 | import com.cmccarthy.ui.utils.DriverHelper; 4 | import org.openqa.selenium.By; 5 | import org.openqa.selenium.WebElement; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public abstract class AbstractStep { 11 | 12 | @Autowired 13 | private DriverHelper driverHelper; 14 | 15 | public void clickAction(WebElement element) throws NoSuchFieldException { 16 | driverHelper.clickAction(element); 17 | } 18 | 19 | public boolean isElementDisplayed(WebElement element) { 20 | return driverHelper.isElementDisplayed(element); 21 | } 22 | 23 | protected void click(WebElement element) throws NoSuchFieldException { 24 | driverHelper.click(element); 25 | } 26 | 27 | protected void click(By locator) throws NoSuchFieldException { 28 | driverHelper.click(locator); 29 | } 30 | 31 | protected void clickAction(By locator) throws NoSuchFieldException { 32 | driverHelper.clickAction(locator); 33 | } 34 | 35 | protected void sendKeys(WebElement element, String value) { 36 | driverHelper.sendKeys(element, value); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/step/CommonPageSteps.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.step; 2 | 3 | import com.cmccarthy.common.utils.LogManager; 4 | import com.cmccarthy.ui.page.WikipediaCommonPage; 5 | import io.cucumber.java.en.Then; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | 8 | import static org.testng.AssertJUnit.assertTrue; 9 | 10 | public class CommonPageSteps extends AbstractStep { 11 | @Autowired 12 | private LogManager logManager; 13 | @Autowired 14 | private WikipediaCommonPage wikipediaCommonPage; 15 | 16 | @Then("The user should be on the Common page") 17 | public void theUserShouldBeOnTheCommonPage() { 18 | logManager.info("The Wikipedia Common page should be opened"); 19 | assertTrue("Wikipedia Common page should be opened", 20 | isElementDisplayed(wikipediaCommonPage.getCentralLogo())); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.step; 2 | 3 | import com.cmccarthy.common.utils.HookUtil; 4 | import com.cmccarthy.common.utils.LogManager; 5 | import com.cmccarthy.ui.config.WikipediaAbstractTestDefinition; 6 | import com.cmccarthy.ui.utils.DriverManager; 7 | import io.cucumber.java.After; 8 | import io.cucumber.java.Before; 9 | import io.cucumber.java.Scenario; 10 | import io.cucumber.spring.CucumberContextConfiguration; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import java.io.IOException; 14 | 15 | @CucumberContextConfiguration 16 | public class Hooks extends WikipediaAbstractTestDefinition { 17 | @Autowired 18 | private LogManager logManager; 19 | @Autowired 20 | private HookUtil hookUtil; 21 | @Autowired 22 | private DriverManager driverManager; 23 | 24 | @Before 25 | public void beforeScenario(Scenario scenario) throws IOException { 26 | String filename = scenario.getName().replaceAll("\\s+", "_"); 27 | logManager.createNewLogger(filename); 28 | driverManager.createDriver(); 29 | } 30 | 31 | @After 32 | public void afterScenario(Scenario scenario) { 33 | hookUtil.endOfTest(scenario); 34 | if (driverManager.getDriver() != null) { 35 | driverManager.getDriver().quit(); 36 | driverManager.setDriver(null); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/step/WikipediaPageSteps.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.step; 2 | 3 | import com.cmccarthy.common.utils.ApplicationProperties; 4 | import com.cmccarthy.common.utils.LogManager; 5 | import com.cmccarthy.ui.page.WikipediaHomePage; 6 | import io.cucumber.java.en.And; 7 | import io.cucumber.java.en.Given; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | 10 | import static org.testng.AssertJUnit.assertTrue; 11 | 12 | public class WikipediaPageSteps extends AbstractStep { 13 | @Autowired 14 | private LogManager logManager; 15 | private final ApplicationProperties applicationProperties; 16 | private final WikipediaHomePage wikipediaHomePage; 17 | 18 | public WikipediaPageSteps(ApplicationProperties applicationProperties, 19 | WikipediaHomePage wikipediaHomePage) { 20 | this.applicationProperties = applicationProperties; 21 | this.wikipediaHomePage = wikipediaHomePage; 22 | } 23 | 24 | @Given("The user opened the Wikipedia Homepage") 25 | public void userIsOpenMainPage() throws NoSuchFieldException { 26 | wikipediaHomePage.open(applicationProperties.getWikipediaUrl()); 27 | logManager.info("The user navigated to the Wikipedia Homepage : " + applicationProperties 28 | .getWikipediaUrl()); 29 | assertTrue("Wikipedia Homepage should be opened", 30 | isElementDisplayed(wikipediaHomePage.getCommonPage())); 31 | } 32 | 33 | @And("The user clicked on the Common link") 34 | public void theUserClickedOnTheCommonLink() throws NoSuchFieldException { 35 | click(wikipediaHomePage.getCommonPage()); 36 | logManager.info("The user clicked the Common link on the Homepage"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverHelper.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.ElementNotInteractableException; 5 | import org.openqa.selenium.StaleElementReferenceException; 6 | import org.openqa.selenium.WebElement; 7 | import org.openqa.selenium.interactions.Actions; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.retry.RetryException; 12 | import org.springframework.retry.annotation.Backoff; 13 | import org.springframework.retry.annotation.Retryable; 14 | import org.springframework.stereotype.Component; 15 | 16 | @Component 17 | @SuppressWarnings("unused") 18 | public class DriverHelper { 19 | 20 | private final Logger logger = LoggerFactory.getLogger(DriverHelper.class); 21 | private DriverManager driverManager; 22 | @Autowired 23 | private DriverWait driverWait; 24 | 25 | /** 26 | * Send Keys to the specified element, clears the element first 27 | */ 28 | public void sendKeys(WebElement element, String value) { 29 | if (value != null) { 30 | if (!value.isEmpty()) { 31 | clear(element); 32 | element.sendKeys(value); 33 | } else { 34 | clear(element); 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Clicks on an element by WebElement 41 | */ 42 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) 43 | public void click(WebElement element) throws NoSuchFieldException { 44 | try { 45 | driverWait.waitForElementToLoad(element); 46 | element.click(); 47 | } catch (StaleElementReferenceException e) { 48 | logger.warn("Could not click on the element"); 49 | throw new RetryException("Could not click on the element : " + element); 50 | } 51 | } 52 | 53 | /** 54 | * Clicks on an element by Locator 55 | */ 56 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) 57 | public void click(By locator) throws NoSuchFieldException { 58 | try { 59 | driverWait.waitForElementToLoad(locator); 60 | driverManager.getDriver().findElement(locator).click(); 61 | } catch (StaleElementReferenceException e) { 62 | logger.warn("Could not click on the element"); 63 | throw new RetryException("Could not click on the element"); 64 | } 65 | } 66 | 67 | /** 68 | * Clicks on an element by Locator 69 | */ 70 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = { RetryException.class }) 71 | public void rightClick(By locator) throws NoSuchFieldException { 72 | driverWait.waitForElementToLoad(locator); 73 | final WebElement element = driverManager.getDriver().findElement(locator); 74 | try { 75 | final Actions builder = new Actions(driverManager.getDriver()); 76 | builder.moveToElement(element).contextClick(element); 77 | builder.perform(); 78 | } catch (Exception ser) { 79 | logger.warn("Could not right click on the element : " + element); 80 | throw new RetryException("Could not click on the element : " + element); 81 | } 82 | } 83 | 84 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) 85 | public void scrollElementIntoView(WebElement element) { 86 | try { 87 | driverManager.getJSExecutor().executeScript("arguments[0].scrollIntoView(true);", element); 88 | } catch (Exception ignored) { 89 | logger.warn("Could not scroll the element into view" + element); 90 | throw new RetryException("Could not click on the element : " + element); 91 | } 92 | } 93 | 94 | /** 95 | * Clicks on an element by WebElement 96 | */ 97 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) 98 | public void rightClick(WebElement element) throws NoSuchFieldException { 99 | driverWait.waitForElementToLoad(element); 100 | 101 | try { 102 | final Actions builder = new Actions(driverManager.getDriver()); 103 | builder.moveToElement(element).contextClick(element); 104 | builder.perform(); 105 | } catch (Exception ser) { 106 | logger.warn("Could not click on the element : " + element); 107 | throw new RetryException("Could not click on the element : " + element); 108 | } 109 | } 110 | 111 | /** 112 | * Clicks on an element using Actions 113 | */ 114 | 115 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) 116 | public void clickAction(WebElement element) throws NoSuchFieldException { 117 | driverWait.waitForElementToLoad(element); 118 | try { 119 | final Actions builder = new Actions(driverManager.getDriver()); 120 | builder.moveToElement(element).click(element); 121 | builder.perform(); 122 | } catch (Exception ser) { 123 | logger.warn("Could not click action on the element"); 124 | throw new RetryException("Could not click on the element : " + element); 125 | } 126 | } 127 | 128 | /** 129 | * Clicks on an element using Actions 130 | */ 131 | @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) 132 | public void clickAction(By locator) throws NoSuchFieldException { 133 | driverWait.waitForElementToLoad(locator); 134 | 135 | final WebElement element = driverManager.getDriver().findElement(locator); 136 | try { 137 | final Actions builder = new Actions(driverManager.getDriver()); 138 | builder.moveToElement(element).click(element); 139 | builder.perform(); 140 | } catch (Exception ser) { 141 | logger.warn("Could not click action on the element"); 142 | throw new RetryException("Could not click on the element : " + element); 143 | } 144 | } 145 | 146 | /** 147 | * Checks if the specified element is displayed 148 | */ 149 | public boolean isElementDisplayed(WebElement element) { 150 | boolean present = false; 151 | try { 152 | present = element.isDisplayed(); 153 | } catch (Exception ignored) { 154 | } 155 | return present; 156 | } 157 | 158 | /** 159 | * Clear text from a field 160 | */ 161 | private void clear(WebElement element) { 162 | try { 163 | driverManager.getJSExecutor().executeScript("arguments[0].value='';", element); 164 | } catch (ElementNotInteractableException ignored) { 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils; 2 | 3 | import com.cmccarthy.common.utils.ApplicationProperties; 4 | import com.cmccarthy.common.utils.Constants; 5 | import org.openqa.selenium.Capabilities; 6 | import org.openqa.selenium.JavascriptExecutor; 7 | import org.openqa.selenium.WebDriver; 8 | import org.openqa.selenium.chrome.ChromeDriver; 9 | import org.openqa.selenium.chrome.ChromeOptions; 10 | import org.openqa.selenium.edge.EdgeDriver; 11 | import org.openqa.selenium.edge.EdgeOptions; 12 | import org.openqa.selenium.firefox.FirefoxDriver; 13 | import org.openqa.selenium.firefox.FirefoxOptions; 14 | import org.openqa.selenium.ie.InternetExplorerDriver; 15 | import org.openqa.selenium.ie.InternetExplorerOptions; 16 | import org.openqa.selenium.remote.RemoteWebDriver; 17 | import org.openqa.selenium.safari.SafariDriver; 18 | import org.openqa.selenium.safari.SafariOptions; 19 | import org.openqa.selenium.support.ui.WebDriverWait; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.core.env.Environment; 24 | import org.springframework.stereotype.Component; 25 | 26 | import java.io.IOException; 27 | import java.net.URL; 28 | import java.time.Duration; 29 | import java.util.Arrays; 30 | import java.util.NoSuchElementException; 31 | 32 | @Component 33 | public class DriverManager { 34 | 35 | private static final ThreadLocal driverThreadLocal = new ThreadLocal<>(); 36 | private final Logger log = LoggerFactory.getLogger(DriverManager.class); 37 | 38 | @Autowired 39 | private ApplicationProperties applicationProperties; 40 | @Autowired 41 | private DriverWait driverWait; 42 | @Autowired 43 | private Environment environment; 44 | 45 | public void createDriver() throws IOException { 46 | if (getDriver() == null) { 47 | if (Arrays.toString(this.environment.getActiveProfiles()).contains("cloud-provider")) { 48 | setRemoteDriver(new URL(applicationProperties.getGridUrl())); 49 | } else { 50 | setLocalWebDriver(); 51 | } 52 | getDriver().manage().deleteAllCookies();//useful for AJAX pages 53 | } 54 | } 55 | 56 | public void setLocalWebDriver() throws IOException { 57 | switch (applicationProperties.getBrowser()) { 58 | case ("chrome") -> { 59 | ChromeOptions options = new ChromeOptions(); 60 | options.addArguments("--disable-logging"); 61 | options.addArguments("--no-sandbox"); 62 | options.addArguments("--disable-dev-shm-usage"); 63 | options.addArguments("--start-maximized"); 64 | options.addArguments("--headless=new"); 65 | driverThreadLocal.set(new ChromeDriver(options)); 66 | } 67 | case ("firefox") -> { 68 | FirefoxOptions firefoxOptions = new FirefoxOptions(); 69 | firefoxOptions.setCapability("marionette", true); 70 | driverThreadLocal.set(new FirefoxDriver(firefoxOptions)); 71 | } 72 | case ("ie") -> { 73 | InternetExplorerOptions capabilitiesIE = new InternetExplorerOptions(); 74 | capabilitiesIE.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true); 75 | driverThreadLocal.set(new InternetExplorerDriver(capabilitiesIE)); 76 | } 77 | case ("safari") -> { 78 | SafariOptions operaOptions = new SafariOptions(); 79 | driverThreadLocal.set(new SafariDriver(operaOptions)); 80 | } 81 | case ("edge") -> { 82 | EdgeOptions edgeOptions = new EdgeOptions(); 83 | driverThreadLocal.set(new EdgeDriver(edgeOptions)); 84 | } 85 | default -> 86 | throw new NoSuchElementException("Failed to create an instance of WebDriver for: " + applicationProperties.getBrowser()); 87 | } 88 | driverWait.getDriverWaitThreadLocal() 89 | .set(new WebDriverWait(driverThreadLocal.get(), Duration.ofSeconds(Constants.timeoutShort), Duration.ofSeconds(Constants.pollingShort))); 90 | } 91 | 92 | private void setRemoteDriver(URL hubUrl) { 93 | Capabilities capability; 94 | switch (applicationProperties.getBrowser()) { 95 | case "firefox" -> { 96 | capability = new FirefoxOptions(); 97 | driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); 98 | } 99 | case "chrome" -> { 100 | ChromeOptions options = new ChromeOptions(); 101 | options.addArguments("--no-sandbox"); 102 | options.addArguments("--disable-dev-shm-usage"); 103 | options.addArguments("--headless"); 104 | driverThreadLocal.set(new RemoteWebDriver(hubUrl, options)); 105 | } 106 | case "ie" -> { 107 | capability = new InternetExplorerOptions(); 108 | driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); 109 | } 110 | case "safari" -> { 111 | capability = new SafariOptions(); 112 | driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); 113 | } 114 | case ("edge") -> { 115 | capability = new EdgeOptions(); 116 | driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); 117 | } 118 | default -> 119 | throw new NoSuchElementException("Failed to create an instance of RemoteWebDriver for: " + applicationProperties.getBrowser()); 120 | } 121 | driverWait.getDriverWaitThreadLocal() 122 | .set(new WebDriverWait(driverThreadLocal.get(), Duration.ofSeconds(Constants.timeoutShort), Duration.ofSeconds(Constants.pollingShort))); 123 | } 124 | 125 | public WebDriver getDriver() { 126 | return driverThreadLocal.get(); 127 | } 128 | 129 | public void setDriver(WebDriver driver) { 130 | driverThreadLocal.set(driver); 131 | } 132 | 133 | public JavascriptExecutor getJSExecutor() { 134 | return (JavascriptExecutor) getDriver(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverWait.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils; 2 | 3 | import com.cmccarthy.common.utils.Constants; 4 | import com.cmccarthy.ui.utils.expectedConditions.*; 5 | import org.openqa.selenium.By; 6 | import org.openqa.selenium.StaleElementReferenceException; 7 | import org.openqa.selenium.WebDriver; 8 | import org.openqa.selenium.WebElement; 9 | import org.openqa.selenium.support.ui.ExpectedCondition; 10 | import org.openqa.selenium.support.ui.FluentWait; 11 | import org.openqa.selenium.support.ui.Wait; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.context.annotation.Lazy; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.time.Duration; 17 | import java.util.NoSuchElementException; 18 | 19 | @Component 20 | public class DriverWait { 21 | 22 | private static final ThreadLocal> driverWaitThreadLocal = new ThreadLocal<>(); 23 | @Autowired 24 | @Lazy 25 | private DriverManager driverManager; 26 | 27 | public ThreadLocal> getDriverWaitThreadLocal() { 28 | return driverWaitThreadLocal; 29 | } 30 | 31 | public void waitForAngular() { 32 | waitUntilAngularReady(); 33 | } 34 | 35 | public void waitForElementToLoad(WebElement element) throws NoSuchFieldException { 36 | waitForAngular(); 37 | waitForElementVisible(element); 38 | waitForElementClickable(element); 39 | } 40 | 41 | public void waitForElementToLoad(By locator) throws NoSuchFieldException { 42 | waitForAngular(); 43 | waitForElementVisible(locator); 44 | waitForElementClickable(locator); 45 | } 46 | 47 | /** 48 | * wait for element visible by element 49 | */ 50 | private void waitForElementVisible(WebElement element) { 51 | try { 52 | waitLong().until(new VisibilityOfElement(element)); 53 | } catch (Exception ignored) { 54 | } 55 | } 56 | 57 | /** 58 | * wait for element visible by locator 59 | */ 60 | private void waitForElementVisible(By locator) { 61 | try { 62 | waitLong().until(new VisibilityOfElementByLocator(locator)); 63 | } catch (Exception ignored) { 64 | } 65 | } 66 | 67 | /** 68 | * wait for element Invisible by locator 69 | */ 70 | private void waitForElementInVisible(By locator) { 71 | try { 72 | waitLong().until(new InvisibilityOfElementByLocator(locator)); 73 | } catch (Exception ignored) { 74 | } 75 | } 76 | 77 | /** 78 | * wait for element Invisible by locator 79 | */ 80 | private void waitForElementInVisible(WebElement element) { 81 | try { 82 | waitLong().until(new InvisibilityOfElement(element)); 83 | } catch (Exception ignored) { 84 | } 85 | } 86 | 87 | /** 88 | * wait for element clickable by element 89 | */ 90 | private void waitForElementClickable(WebElement element) throws NoSuchFieldException { 91 | try { 92 | waitLong().until(new ClickabilityOfElement(element)); 93 | } catch (Exception t) { 94 | throw new NoSuchFieldException("could not interact with the element " + element); 95 | } 96 | } 97 | 98 | /** 99 | * wait for element clickable by locator 100 | */ 101 | private void waitForElementClickable(By locator) throws NoSuchFieldException { 102 | try { 103 | waitLong().until(new ClickabilityOfElementByLocator(locator)); 104 | } catch (Exception t) { 105 | throw new NoSuchFieldException("could not interact with the element by locator " + locator); 106 | } 107 | } 108 | 109 | public Wait waitLong() { 110 | return new FluentWait<>(driverManager.getDriver()) 111 | .withTimeout(Duration.ofSeconds(Constants.timeoutLong)) 112 | .pollingEvery(Duration.ofMillis(Constants.pollingLong)) 113 | .ignoring(NoSuchElementException.class, StaleElementReferenceException.class); 114 | } 115 | 116 | public Wait waitShort() { 117 | return new FluentWait<>(driverManager.getDriver()) 118 | .withTimeout(Duration.ofSeconds(Constants.timeoutShort)) 119 | .pollingEvery(Duration.ofMillis(Constants.pollingShort)) 120 | .ignoring(NoSuchElementException.class, StaleElementReferenceException.class); 121 | } 122 | 123 | private void waitUntilAngularReady() { 124 | 125 | 126 | final Boolean angularUnDefined = (Boolean) driverManager.getJSExecutor() 127 | .executeScript("return window.angular === undefined"); 128 | 129 | if (!angularUnDefined) { 130 | Boolean angularInjectorUnDefined = (Boolean) driverManager.getJSExecutor() 131 | .executeScript("return angular.element(document).injector() === undefined"); 132 | if (!angularInjectorUnDefined) { 133 | waitForAngularLoad(); 134 | waitUntilJSReady(); 135 | waitForJQueryLoad(); 136 | } 137 | } 138 | } 139 | 140 | private void waitForAngularLoad() { 141 | final String angularReadyScript = "return angular.element(document).injector().get('$http').pendingRequests.length === 0"; 142 | final ExpectedCondition angularLoad = driver -> Boolean.valueOf( 143 | (driverManager.getJSExecutor()).executeScript(angularReadyScript).toString()); 144 | boolean angularReady = Boolean 145 | .parseBoolean(driverManager.getJSExecutor().executeScript(angularReadyScript).toString()); 146 | if (!angularReady) { 147 | waitLong().until(angularLoad); 148 | } 149 | } 150 | 151 | private void waitUntilJSReady() { 152 | final ExpectedCondition jsLoad = driver -> (driverManager.getJSExecutor()) 153 | .executeScript("return document.readyState") 154 | .toString() 155 | .equals("complete"); 156 | 157 | boolean jsReady = driverManager.getJSExecutor().executeScript("return document.readyState") 158 | .toString().equals("complete"); 159 | 160 | if (!jsReady) { 161 | waitLong().until(jsLoad); 162 | } 163 | } 164 | 165 | private void waitForJQueryLoad() { 166 | final ExpectedCondition jQueryLoad = driver -> ( 167 | (Long) (driverManager.getJSExecutor()).executeScript("return jQuery.active") == 0); 168 | boolean jqueryReady = (Boolean) driverManager.getJSExecutor() 169 | .executeScript("return jQuery.active==0"); 170 | if (!jqueryReady) { 171 | waitLong().until(jQueryLoad); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/expectedConditions/ClickabilityOfElement.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils.expectedConditions; 2 | 3 | import com.cmccarthy.common.utils.Constants; 4 | import org.openqa.selenium.NoSuchElementException; 5 | import org.openqa.selenium.StaleElementReferenceException; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.WebElement; 8 | import org.openqa.selenium.support.ui.ExpectedCondition; 9 | import org.openqa.selenium.support.ui.ExpectedConditions; 10 | import org.openqa.selenium.support.ui.FluentWait; 11 | import org.openqa.selenium.support.ui.Wait; 12 | 13 | import java.time.Duration; 14 | 15 | public class ClickabilityOfElement implements ExpectedCondition { 16 | 17 | private final WebElement element; 18 | 19 | public ClickabilityOfElement(WebElement element) { 20 | this.element = element; 21 | } 22 | 23 | @Override 24 | public WebElement apply(WebDriver webDriver) { 25 | 26 | final Wait wait = new FluentWait<>(webDriver) 27 | .withTimeout(Duration.ofSeconds(Constants.timeoutShort)) 28 | .pollingEvery(Duration.ofMillis(Constants.pollingShort)) 29 | .ignoring(java.util.NoSuchElementException.class, 30 | StaleElementReferenceException.class); 31 | try { 32 | return wait.until(ExpectedConditions.elementToBeClickable(element)); 33 | } catch (StaleElementReferenceException | NoSuchElementException exception) { 34 | return element; 35 | } catch (Throwable t) { 36 | throw new Error(t); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/expectedConditions/ClickabilityOfElementByLocator.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils.expectedConditions; 2 | 3 | import com.cmccarthy.common.utils.Constants; 4 | import org.openqa.selenium.*; 5 | import org.openqa.selenium.support.ui.ExpectedCondition; 6 | import org.openqa.selenium.support.ui.ExpectedConditions; 7 | import org.openqa.selenium.support.ui.FluentWait; 8 | import org.openqa.selenium.support.ui.Wait; 9 | 10 | import java.time.Duration; 11 | 12 | public class ClickabilityOfElementByLocator implements ExpectedCondition { 13 | 14 | private final By locator; 15 | 16 | public ClickabilityOfElementByLocator(By locator) { 17 | this.locator = locator; 18 | } 19 | 20 | @Override 21 | public WebElement apply(WebDriver webDriver) { 22 | 23 | final Wait wait = new FluentWait<>(webDriver) 24 | .withTimeout(Duration.ofSeconds(Constants.timeoutShort)) 25 | .pollingEvery(Duration.ofMillis(Constants.pollingShort)) 26 | .ignoring(java.util.NoSuchElementException.class, 27 | StaleElementReferenceException.class); 28 | 29 | try { 30 | return wait.until(ExpectedConditions.elementToBeClickable(locator)); 31 | } catch (StaleElementReferenceException | NoSuchElementException e) { 32 | return webDriver.findElement(locator); 33 | } catch (Throwable t) { 34 | throw new Error(t); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/expectedConditions/InvisibilityOfElement.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils.expectedConditions; 2 | 3 | import org.openqa.selenium.NoSuchElementException; 4 | import org.openqa.selenium.StaleElementReferenceException; 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.WebElement; 7 | import org.openqa.selenium.support.ui.ExpectedCondition; 8 | 9 | public class InvisibilityOfElement implements ExpectedCondition { 10 | 11 | private final WebElement element; 12 | 13 | public InvisibilityOfElement(WebElement element) { 14 | this.element = element; 15 | } 16 | 17 | @Override 18 | public Boolean apply(WebDriver d) { 19 | try { 20 | return element.isDisplayed(); 21 | } catch (StaleElementReferenceException | NoSuchElementException e) { 22 | return true; 23 | } catch (Throwable t) { 24 | throw new Error(t); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/expectedConditions/InvisibilityOfElementByLocator.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils.expectedConditions; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.NoSuchElementException; 5 | import org.openqa.selenium.StaleElementReferenceException; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.support.ui.ExpectedCondition; 8 | 9 | public class InvisibilityOfElementByLocator implements ExpectedCondition { 10 | 11 | private final By locator; 12 | 13 | public InvisibilityOfElementByLocator(By locator) { 14 | this.locator = locator; 15 | } 16 | 17 | @Override 18 | public Boolean apply(WebDriver d) { 19 | try { 20 | return d.findElement(locator).isDisplayed(); 21 | } catch (StaleElementReferenceException | NoSuchElementException e) { 22 | return true; 23 | } catch (Throwable t) { 24 | throw new Error(t); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/expectedConditions/VisibilityOfElement.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils.expectedConditions; 2 | 3 | import org.openqa.selenium.NoSuchElementException; 4 | import org.openqa.selenium.StaleElementReferenceException; 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.WebElement; 7 | import org.openqa.selenium.support.ui.ExpectedCondition; 8 | 9 | public class VisibilityOfElement implements ExpectedCondition { 10 | 11 | private final WebElement element; 12 | 13 | public VisibilityOfElement(WebElement element) { 14 | this.element = element; 15 | } 16 | 17 | @Override 18 | public Boolean apply(WebDriver d) { 19 | try { 20 | return element.isDisplayed(); 21 | } catch (StaleElementReferenceException | NoSuchElementException e) { 22 | return false; 23 | } catch (Throwable t) { 24 | throw new Error(t); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /wikipedia/src/test/java/com/cmccarthy/ui/utils/expectedConditions/VisibilityOfElementByLocator.java: -------------------------------------------------------------------------------- 1 | package com.cmccarthy.ui.utils.expectedConditions; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.NoSuchElementException; 5 | import org.openqa.selenium.StaleElementReferenceException; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.support.ui.ExpectedCondition; 8 | 9 | public class VisibilityOfElementByLocator implements ExpectedCondition { 10 | 11 | private final By locator; 12 | 13 | public VisibilityOfElementByLocator(By locator) { 14 | this.locator = locator; 15 | } 16 | 17 | @Override 18 | public Boolean apply(WebDriver d) { 19 | try { 20 | return d.findElement(locator).isDisplayed(); 21 | } catch (StaleElementReferenceException | NoSuchElementException e) { 22 | return false; 23 | } catch (Throwable t) { 24 | throw new Error(t); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /wikipedia/src/test/resources/cucumber.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true -------------------------------------------------------------------------------- /wikipedia/src/test/resources/feature/WikipediaTest.feature: -------------------------------------------------------------------------------- 1 | @Wikipedia-UI 2 | Feature: As a user, I want to look for an article on Wikipedia 3 | 4 | Scenario: I want to find an article on Wikipedia 1 5 | Given The user opened the Wikipedia Homepage 6 | And The user clicked on the Common link 7 | Then The user should be on the Common page 8 | 9 | Scenario: I want to find an article on Wikipedia 1.1 10 | Given The user opened the Wikipedia Homepage 11 | And The user clicked on the Common link 12 | Then The user should be on the Common page 13 | 14 | 15 | -------------------------------------------------------------------------------- /wikipedia/src/test/resources/feature/WikipediaTest2.feature: -------------------------------------------------------------------------------- 1 | @Wikipedia-UI 2 | Feature: As a user, I want to look for an article on Wikipedia 2 3 | 4 | Scenario: I want to find an article on Wikipedia 2 5 | Given The user opened the Wikipedia Homepage 6 | And The user clicked on the Common link 7 | Then The user should be on the Common page 8 | -------------------------------------------------------------------------------- /wikipedia/src/test/resources/suite/WikipediaSuiteTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------