From 1740e859cea1c59257f550083b29663d65e168e4 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 12:35:27 +1000 Subject: [PATCH 01/28] init commit --- README.md | 383 ------------------ .../cmccarthy/common/service/RestService.java | 92 ++++- .../application-headless-github.properties | 26 ++ pom.xml | 35 ++ .../test/resources/suite/WeatherSuiteTest.xml | 4 +- .../config/WikipediaContextConfiguration.java | 5 +- .../java/com/cmccarthy/ui/step/Hooks.java | 18 +- .../com/cmccarthy/ui/utils/DriverManager.java | 203 ++++++++-- .../resources/suite/WikipediaSuiteTest.xml | 4 +- 9 files changed, 342 insertions(+), 428 deletions(-) diff --git a/README.md b/README.md index 6a8851f..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,383 +0,0 @@ -# Cucumber Automated Testing Framework - -[![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) - -# Index - - - - - - - - - - - - - - - - - -
Start - | Maven - | Quickstart | -
Run - | TestNG - | Command Line - | IDE Support - | Java JDK - | Troubleshooting | -
Report - | Configuration - | Environment Switching - | Spark HTML Reports - | Logging | -
Advanced - | Before / After Hooks - | JSON Transforms - | Contributing | -
- -# Maven - -The Framework uses [Spring Boot Test](https://spring.io/guides/gs/testing-web/), [Cucumber](https://cucumber.io/) -, [Rest Assured](https://rest-assured.io/) and [Selenium](https://www.selenium.dev/) client implementations. - -Spring ``: - -```xml - - - ... - - org.springframework.amqp - spring-rabbit - ${spring-rabbit.version} - - - org.springframework.boot - spring-boot-starter-test - - - org.springframework - spring-test - - ... - -``` - -Cucumber & Rest Assured ``: - -```xml - - - ... - - io.rest-assured - rest-assured - ${restassured.version} - - - io.cucumber - cucumber-java - ${cucumber.version} - - - io.cucumber - cucumber-spring - ${cucumber.version} - - - io.cucumber - cucumber-testng - ${cucumber.version} - - ... - -``` - -Selenium ``: - -```xml - - - ... - - org.seleniumhq.selenium - selenium-java - ${selenium-version} - - - org.seleniumhq.selenium - selenium-server - ${selenium-version} - - ... - -``` - -# Quickstart - -- [Intellij IDE](https://www.jetbrains.com/idea/) - `Recommended` -- [Java JDK 17](https://jdk.java.net/java-se-ri/11) -- [Apache Maven](https://maven.apache.org/docs/3.6.3/release-notes.html) - -# TestNG - -By using the [TestNG Framework](https://junit.org/junit4/) we can utilize the [Cucumber Framework](https://cucumber.io/) -and the `@CucumberOptions` Annotation Type to execute the `*.feature` file tests - -> Right click the `WikipediParallelRunner` class and select `Run` - -```java - -@CucumberOptions( - features = { - "src/test/resources/feature" - }, - plugin = { - "pretty", - "json:target/cucumber/report.json", - "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:" - }) -public class WikipediaParallelRunnerTest extends AbstractTestNGCucumberTests { - - @Override - @DataProvider(parallel = true) - public Object[][] scenarios() { - return super.scenarios(); - } - -} -``` - -# Command Line - -Normally you will use your IDE to run a `*.feature` file directly or via the `*Test.java` class. With the `Test` class, -we can run tests from the command-line as well. - -Note that the `mvn test` command only runs test classes that follow the `*Test.java` naming convention. - -You can run a single test or a suite or tests like so : - -``` -mvn test -Dtest=WikipediaParallelRunnerTest -``` - -Note that the `mvn clean install` command runs all test Classes that follow the `*Test.java` naming convention - -``` -mvn clean install -``` - -# IDE Support - -To minimize the discrepancies between IDE versions and Locales the `` is set to `UTF-8` - -```xml - - - ... - UTF-8 - UTF-8 - ... - -``` - -# Java JDK - -The Java version to use is defined in the `maven-compiler-plugin` - -```xml - - - ... - - - ... - - org.apache.maven.plugins - maven-compiler-plugin - - 17 - 17 - - - ... - - - ... - -``` - -# Configuration - -The `AbstractTestDefinition` class is responsible for specifying each Step class as `@SpringBootTest` and -its `@ContextConfiguration` - -```java - -@ContextConfiguration(classes = {FrameworkContextConfiguration.class}) -@SpringBootTest -public class AbstractTestDefinition { -} -``` - -The `FrameworkContextConfiguration` class is responsible for specifying the Spring `@Configuration`, modules to scan, -properties to use etc - -```java - -@EnableRetry -@Configuration -@ComponentScan({ - "com.cmccarthy.api", "com.cmccarthy.common", -}) -@PropertySource("application.properties") -public class FrameworkContextConfiguration { -} -``` - -# Environment Switching - -There is only one thing you need to do to switch the environment - which is to set `` property in the -Master POM. - -> By default, the value of `spring.profiles.active` is defined in the `application.properties` file which inherits its -> value from the Master POM property `` - -```xml - - - ... - - prod - - true - - - prod - - - ... - -``` - -You can then specify the profile to use when running Maven from the command line like so: - -``` -mvn clean install -DactiveProfile=github-headless -``` - -Below is an example of the `application.properties` file. - -```properties -spring.profiles.active=@activatedProperties@ -``` - -# Extent Reports - -The Framework uses [Extent Reports Framework](https://extentreports.com/) to generate the HTML Test Reports - -The example below is a report generated automatically by Extent Reports open-source library. - - - -# Allure Reports - -The Framework uses [Allure Reports](https://docs.qameta.io/allure/) to generate the HTML Test Reports - -The example below is a report generated by Allure Reports open-source library. - - - -To generate the above report navigate to the root directory of the module under test and execute the following command - -`mvn allure:serve` or `mvn allure:generate` (for an offline report) - -# Logging - -The Framework uses [Log4j2](https://logging.apache.org/log4j/2.x/) You can instantiate the logging service in any Class -like so - -```java -private final Logger logger=LoggerFactory.getLogger(WikipediaPageSteps.class); -``` - -you can then use the logger like so : - -```java -logger.info("This is a info message"); - logger.warn("This is a warning message"); - logger.debug("This is a info message"); - logger.error("This is a error message"); -``` - -# Before / After Hooks - -The [Logback](http://logback.qos.ch/) logging service is initialized from the `Hooks.class` - -As the Cucumber Hooks are implemented by all steps we can configure the `@CucumberContextConfiguration` like so : - -```java - -@CucumberContextConfiguration -public class Hooks extends AbstractTestDefinition { - - private static boolean initialized = false; - private static final Object lock = new Object(); - - @Autowired - private HookUtil hookUtil; - @Autowired - private DriverManager driverManager; - - @Before - public void beforeScenario(Scenario scenario) { - synchronized (lock) { - if (!initialized) { - if (!driverManager.isDriverExisting()) { - driverManager.downloadDriver(); - } - initialized = true; - } - } - driverManager.createDriver(); - } - - @After - public void afterScenario(Scenario scenario) { - hookUtil.endOfTest(scenario); - WebDriverRunner.closeWebDriver(); - } -} -``` - -# JSON Transforms - -[Rest Assured IO](https://rest-assured.io/) is used to map the `Response` Objects to their respective `POJO` Classes - -```xml - - - io.rest-assured - rest-assured - 3.0.0 - -``` - -# Troubleshooting - -- Execute the following commands to resolve any dependency issues - 1. `cd ~/install directory path/spring-cucumber-testng-parallel-test-harness` - 2. `mvn clean install -DskipTests` - -# Contributing - -Spotted a mistake? Questions? Suggestions? - -[Open an Issue](https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/issues) - - diff --git a/common/src/main/java/com/cmccarthy/common/service/RestService.java b/common/src/main/java/com/cmccarthy/common/service/RestService.java index 5003856..150201b 100644 --- a/common/src/main/java/com/cmccarthy/common/service/RestService.java +++ b/common/src/main/java/com/cmccarthy/common/service/RestService.java @@ -1,14 +1,104 @@ package com.cmccarthy.common.service; +import com.cmccarthy.common.config.TestConfiguration; +import com.cmccarthy.common.utils.LogManager; +import io.restassured.RestAssured; +import io.restassured.config.HttpClientConfig; +import io.restassured.config.RestAssuredConfig; +import io.restassured.filter.log.RequestLoggingFilter; +import io.restassured.filter.log.ResponseLoggingFilter; +import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; +import jakarta.annotation.PostConstruct; import static io.restassured.RestAssured.given; @Service public class RestService { + @Autowired(required = false) + private TestConfiguration testConfiguration; + + @Autowired(required = false) + private LogManager logManager; + + @PostConstruct + public void init() { + if (testConfiguration != null) { + setupRestAssuredConfig(); + } + } + + private void setupRestAssuredConfig() { + TestConfiguration.ApiConfig apiConfig = testConfiguration.getApi(); + + RestAssuredConfig config = RestAssuredConfig.config() + .httpClient(HttpClientConfig.httpClientConfig() + .setParam("http.connection.timeout", apiConfig.getConnectionTimeout()) + .setParam("http.socket.timeout", apiConfig.getSocketTimeout())); + + RestAssured.config = config; + + if (apiConfig.isLogRequestResponse() && logManager != null) { + RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); + } + } + public RequestSpecification getRequestSpecification() { - return given().header("Content-Type", "application/json"); + return given() + .header("Content-Type", "application/json") + .accept("application/json"); + } + + @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) + public Response executeWithRetry(RequestSpecification request, String method, String endpoint) { + if (logManager != null) { + logManager.info("Executing " + method + " request to: " + endpoint); + } + + Response response; + switch (method.toUpperCase()) { + case "GET": + response = request.get(endpoint); + break; + case "POST": + response = request.post(endpoint); + break; + case "PUT": + response = request.put(endpoint); + break; + case "DELETE": + response = request.delete(endpoint); + break; + default: + throw new IllegalArgumentException("Unsupported HTTP method: " + method); + } + + if (logManager != null) { + logManager.info("Response status: " + response.getStatusCode()); + logManager.debug("Response body: " + response.getBody().asString()); + } + + return response; + } + + public Response get(String endpoint) { + return executeWithRetry(getRequestSpecification(), "GET", endpoint); + } + + public Response post(String endpoint, Object body) { + return executeWithRetry(getRequestSpecification().body(body), "POST", endpoint); + } + + public Response put(String endpoint, Object body) { + return executeWithRetry(getRequestSpecification().body(body), "PUT", endpoint); + } + + public Response delete(String endpoint) { + return executeWithRetry(getRequestSpecification(), "DELETE", endpoint); } } diff --git a/common/src/main/resources/application-headless-github.properties b/common/src/main/resources/application-headless-github.properties index b0280bc..f26d86e 100644 --- a/common/src/main/resources/application-headless-github.properties +++ b/common/src/main/resources/application-headless-github.properties @@ -2,3 +2,29 @@ weather.url.value=http://api.openweathermap.org/data/2.5/weather wikipedia.url.value=https://www.wikipedia.org/ browser=chrome github=true + +# Test Configuration +test.max-retries=3 +test.thread-count=4 +test.timeout-seconds=30 +test.take-screenshot-on-failure=true +test.enable-detailed-reporting=true +test.default-browser=chrome + +# Parallel Execution Settings +test.parallel-execution.enabled=true +test.parallel-execution.thread-pool-size=4 +test.parallel-execution.data-provider-thread-count=4 + +# API Configuration +test.api.connection-timeout=30000 +test.api.socket-timeout=30000 +test.api.max-retries=3 +test.api.log-request-response=false + +# UI Configuration +test.ui.headless=true +test.ui.implicit-wait=10 +test.ui.page-load-timeout=30 +test.ui.window-size=1920x1080 +test.ui.enable-video-recording=false diff --git a/pom.xml b/pom.xml index 8dd8df1..41ce6ad 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,9 @@ 1.7.0 3.1.2 3.2.3 + 2.18.2 + 3.0.0 + 4.2.2 @@ -147,6 +150,11 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-configuration-processor + true + org.springframework.boot spring-boot-starter-test @@ -171,6 +179,33 @@ spring-boot-starter-aop + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + + + jakarta.annotation + jakarta.annotation-api + ${jakarta.annotation.version} + + + + + org.awaitility + awaitility + ${awaitility.version} + test + + net.sf.jtidy diff --git a/weather/src/test/resources/suite/WeatherSuiteTest.xml b/weather/src/test/resources/suite/WeatherSuiteTest.xml index 79361eb..3bfb9f7 100644 --- a/weather/src/test/resources/suite/WeatherSuiteTest.xml +++ b/weather/src/test/resources/suite/WeatherSuiteTest.xml @@ -1,8 +1,8 @@ - + - + diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java b/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java index 131b518..58e03ed 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java @@ -7,7 +7,10 @@ @EnableRetry @Configuration -@ComponentScan({"com.cmccarthy.ui", "com.cmccarthy.common"}) +@ComponentScan({ + "com.cmccarthy.ui", + "com.cmccarthy.common" +}) @PropertySource("classpath:/application.properties") public class WikipediaContextConfiguration { } diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java index 17446cf..ac9b43b 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java @@ -14,6 +14,10 @@ @CucumberContextConfiguration public class Hooks extends WikipediaAbstractTestDefinition { + + private static boolean initialized = false; + private static final Object lock = new Object(); + @Autowired private LogManager logManager; @Autowired @@ -23,6 +27,15 @@ public class Hooks extends WikipediaAbstractTestDefinition { @Before public void beforeScenario(Scenario scenario) throws IOException { + synchronized (lock) { + if (!initialized) { + if (!driverManager.isDriverExisting()) { + driverManager.downloadDriver(); + } + initialized = true; + } + } + String filename = scenario.getName().replaceAll("\\s+", "_"); logManager.createNewLogger(filename); driverManager.createDriver(); @@ -31,9 +44,6 @@ public void beforeScenario(Scenario scenario) throws IOException { @After public void afterScenario(Scenario scenario) { hookUtil.endOfTest(scenario); - if (driverManager.getDriver() != null) { - driverManager.getDriver().quit(); - driverManager.setDriver(null); - } + driverManager.quitDriver(); } } \ No newline at end of file diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index 30bdb0b..407347e 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -1,5 +1,6 @@ package com.cmccarthy.ui.utils; +import com.cmccarthy.common.config.TestConfiguration; import com.cmccarthy.common.utils.ApplicationProperties; import com.cmccarthy.common.utils.Constants; import org.openqa.selenium.Capabilities; @@ -27,6 +28,8 @@ import java.net.URL; import java.time.Duration; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.NoSuchElementException; @Component @@ -41,66 +44,64 @@ public class DriverManager { private DriverWait driverWait; @Autowired private Environment environment; + @Autowired(required = false) + private TestConfiguration testConfiguration; public void createDriver() throws IOException { if (getDriver() == null) { + log.info("Creating WebDriver for browser: {}", applicationProperties.getBrowser()); + if (Arrays.toString(this.environment.getActiveProfiles()).contains("cloud-provider")) { setRemoteDriver(new URL(applicationProperties.getGridUrl())); } else { setLocalWebDriver(); } - getDriver().manage().deleteAllCookies();//useful for AJAX pages + + configureDriver(); } } public void setLocalWebDriver() throws IOException { - switch (applicationProperties.getBrowser()) { - case ("chrome") -> { - ChromeOptions options = new ChromeOptions(); - options.addArguments("--disable-logging"); - options.addArguments("--no-sandbox"); - options.addArguments("--disable-dev-shm-usage"); - options.addArguments("--start-maximized"); - options.addArguments("--headless=new"); - driverThreadLocal.set(new ChromeDriver(options)); + String browser = applicationProperties.getBrowser(); + log.info("Creating local {} driver", browser); + + switch (browser.toLowerCase()) { + case "chrome" -> { + driverThreadLocal.set(new ChromeDriver(getChromeOptions())); } - case ("firefox") -> { - FirefoxOptions firefoxOptions = new FirefoxOptions(); - firefoxOptions.setCapability("marionette", true); - driverThreadLocal.set(new FirefoxDriver(firefoxOptions)); + case "firefox" -> { + driverThreadLocal.set(new FirefoxDriver(getFirefoxOptions())); } - case ("ie") -> { + case "ie" -> { InternetExplorerOptions capabilitiesIE = new InternetExplorerOptions(); capabilitiesIE.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true); driverThreadLocal.set(new InternetExplorerDriver(capabilitiesIE)); } - case ("safari") -> { - SafariOptions operaOptions = new SafariOptions(); - driverThreadLocal.set(new SafariDriver(operaOptions)); + case "safari" -> { + driverThreadLocal.set(new SafariDriver(getSafariOptions())); } - case ("edge") -> { - EdgeOptions edgeOptions = new EdgeOptions(); - driverThreadLocal.set(new EdgeDriver(edgeOptions)); + case "edge" -> { + driverThreadLocal.set(new EdgeDriver(getEdgeOptions())); } default -> - throw new NoSuchElementException("Failed to create an instance of WebDriver for: " + applicationProperties.getBrowser()); + throw new NoSuchElementException("Failed to create an instance of WebDriver for: " + browser); } - driverWait.getDriverWaitThreadLocal() - .set(new WebDriverWait(driverThreadLocal.get(), Duration.ofSeconds(Constants.timeoutShort), Duration.ofSeconds(Constants.pollingShort))); + + setupDriverWait(); } private void setRemoteDriver(URL hubUrl) { + String browser = applicationProperties.getBrowser(); + log.info("Creating remote {} driver at {}", browser, hubUrl); + Capabilities capability; - switch (applicationProperties.getBrowser()) { + switch (browser.toLowerCase()) { case "firefox" -> { - capability = new FirefoxOptions(); + capability = getFirefoxOptions(); driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } case "chrome" -> { - ChromeOptions options = new ChromeOptions(); - options.addArguments("--no-sandbox"); - options.addArguments("--disable-dev-shm-usage"); - options.addArguments("--headless"); + ChromeOptions options = getChromeOptions(); driverThreadLocal.set(new RemoteWebDriver(hubUrl, options)); } case "ie" -> { @@ -108,18 +109,117 @@ private void setRemoteDriver(URL hubUrl) { driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } case "safari" -> { - capability = new SafariOptions(); + capability = getSafariOptions(); driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } - case ("edge") -> { - capability = new EdgeOptions(); + case "edge" -> { + capability = getEdgeOptions(); driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } default -> - throw new NoSuchElementException("Failed to create an instance of RemoteWebDriver for: " + applicationProperties.getBrowser()); + throw new NoSuchElementException("Failed to create an instance of RemoteWebDriver for: " + browser); + } + + setupDriverWait(); + } + + private ChromeOptions getChromeOptions() { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--disable-logging"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + options.addArguments("--disable-gpu"); + options.addArguments("--disable-extensions"); + options.addArguments("--start-maximized"); + + if (isHeadlessMode()) { + options.addArguments("--headless=new"); + } + + if (testConfiguration != null) { + String windowSize = testConfiguration.getUi().getWindowSize(); + if (windowSize != null && !windowSize.isEmpty()) { + options.addArguments("--window-size=" + windowSize); + } + } + + // Performance optimizations + Map prefs = new HashMap<>(); + prefs.put("profile.default_content_setting_values.notifications", 2); + prefs.put("profile.default_content_settings.popups", 0); + options.setExperimentalOption("prefs", prefs); + + return options; + } + + private FirefoxOptions getFirefoxOptions() { + FirefoxOptions options = new FirefoxOptions(); + options.setCapability("marionette", true); + + if (isHeadlessMode()) { + options.addArguments("--headless"); + } + + return options; + } + + private EdgeOptions getEdgeOptions() { + EdgeOptions options = new EdgeOptions(); + options.addArguments("--disable-logging"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + + if (isHeadlessMode()) { + options.addArguments("--headless"); + } + + return options; + } + + private SafariOptions getSafariOptions() { + return new SafariOptions(); + } + + private void configureDriver() { + WebDriver driver = getDriver(); + if (driver != null) { + // Configure timeouts + if (testConfiguration != null) { + TestConfiguration.UiConfig uiConfig = testConfiguration.getUi(); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(uiConfig.getImplicitWait())); + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(uiConfig.getPageLoadTimeout())); + } else { + // Fallback to default values + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(Constants.timeoutShort)); + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(Constants.timeoutLong)); + } + + driver.manage().deleteAllCookies(); // useful for AJAX pages + + if (!isHeadlessMode()) { + driver.manage().window().maximize(); + } + + log.info("WebDriver configured successfully for browser: {}", applicationProperties.getBrowser()); } + } + + private void setupDriverWait() { driverWait.getDriverWaitThreadLocal() - .set(new WebDriverWait(driverThreadLocal.get(), Duration.ofSeconds(Constants.timeoutShort), Duration.ofSeconds(Constants.pollingShort))); + .set(new WebDriverWait(driverThreadLocal.get(), + Duration.ofSeconds(Constants.timeoutShort), + Duration.ofSeconds(Constants.pollingShort))); + } + + private boolean isHeadlessMode() { + if (testConfiguration != null) { + return testConfiguration.getUi().isHeadless(); + } + + // Check system property, environment variable, or active profiles as fallback + return Boolean.parseBoolean(System.getProperty("headless", "false")) || + Boolean.parseBoolean(System.getenv("HEADLESS")) || + Arrays.toString(this.environment.getActiveProfiles()).contains("headless"); } public WebDriver getDriver() { @@ -133,4 +233,37 @@ public void setDriver(WebDriver driver) { public JavascriptExecutor getJSExecutor() { return (JavascriptExecutor) getDriver(); } + + /** + * Check if driver is existing and not null + */ + public boolean isDriverExisting() { + return getDriver() != null; + } + + /** + * Safely quit and clean up the driver + */ + public void quitDriver() { + WebDriver driver = getDriver(); + if (driver != null) { + try { + driver.quit(); + log.info("WebDriver quit successfully"); + } catch (Exception e) { + log.warn("Error while quitting WebDriver: {}", e.getMessage()); + } finally { + driverThreadLocal.remove(); + driverWait.getDriverWaitThreadLocal().remove(); + } + } + } + + /** + * Download driver binaries if needed (placeholder for future enhancement) + */ + public void downloadDriver() { + log.info("Driver download check - using WebDriverManager or system drivers"); + // Future enhancement: integrate with WebDriverManager for automatic driver downloads + } } diff --git a/wikipedia/src/test/resources/suite/WikipediaSuiteTest.xml b/wikipedia/src/test/resources/suite/WikipediaSuiteTest.xml index f196d67..4d851eb 100644 --- a/wikipedia/src/test/resources/suite/WikipediaSuiteTest.xml +++ b/wikipedia/src/test/resources/suite/WikipediaSuiteTest.xml @@ -1,8 +1,8 @@ - + - + From a14dbb3fe211f4c9a0bb17a2ae981b1f91ff8431 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 12:36:38 +1000 Subject: [PATCH 02/28] init commit --- .github/workflows/enhanced-ci.yml | 139 ++++++++++++++++++ .../common/config/TestConfiguration.java | 96 ++++++++++++ .../exception/TestFrameworkException.java | 53 +++++++ .../listeners/TestExecutionListener.java | 55 +++++++ .../common/service/TestDataManager.java | 95 ++++++++++++ 5 files changed, 438 insertions(+) create mode 100644 .github/workflows/enhanced-ci.yml create mode 100644 common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java create mode 100644 common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java create mode 100644 common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java create mode 100644 common/src/main/java/com/cmccarthy/common/service/TestDataManager.java diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml new file mode 100644 index 0000000..16d7710 --- /dev/null +++ b/.github/workflows/enhanced-ci.yml @@ -0,0 +1,139 @@ +name: Enhanced CI/CD Pipeline + +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ master ] + schedule: + - cron: '0 2 * * *' # Daily at 2 AM + +env: + JAVA_VERSION: '17' + MAVEN_OPTS: '-Xmx2048m' + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + strategy: + matrix: + module: [weather, wikipedia] + browser: [chrome, firefox] + fail-fast: false + max-parallel: 4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Setup Chrome + if: matrix.browser == 'chrome' + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + + - name: Setup Firefox + if: matrix.browser == 'firefox' + uses: browser-actions/setup-firefox@v1 + with: + firefox-version: latest + + - name: Compile project + run: mvn clean compile test-compile + + - name: Run ${{ matrix.module }} tests with ${{ matrix.browser }} + run: | + cd ${{ matrix.module }} + mvn test -Dbrowser=${{ matrix.browser }} -Dtest.ui.headless=true -Dspring.profiles.active=headless-github + env: + BROWSER: ${{ matrix.browser }} + HEADLESS: true + + - name: Generate Allure Report + if: always() + run: | + cd ${{ matrix.module }} + mvn allure:report + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.module }}-${{ matrix.browser }} + path: | + ${{ matrix.module }}/target/allure-results/ + ${{ matrix.module }}/target/cucumber/ + ${{ matrix.module }}/logs/ + retention-days: 30 + + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots-${{ matrix.module }}-${{ matrix.browser }} + path: ${{ matrix.module }}/screenshots/ + retention-days: 7 + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Snyk security scan + uses: snyk/actions/maven@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high + + publish-reports: + name: Publish Test Reports + needs: test + runs-on: ubuntu-latest + if: always() + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Publish Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: Test Results + path: '**/target/cucumber/*.json' + reporter: java-junit + fail-on-error: false + + notification: + name: Notify Results + needs: [test, security-scan] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Notify Slack + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + channel: '#testing' + webhook_url: ${{ secrets.SLACK_WEBHOOK }} + fields: repo,message,commit,author,action,eventName,ref,workflow diff --git a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java new file mode 100644 index 0000000..f0158d1 --- /dev/null +++ b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java @@ -0,0 +1,96 @@ +package com.cmccarthy.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "test") +public class TestConfiguration { + + private int maxRetries = 3; + private int threadCount = 4; + private int timeoutSeconds = 30; + private boolean takeScreenshotOnFailure = true; + private boolean enableDetailedReporting = true; + private String defaultBrowser = "chrome"; + + // Parallel execution settings + private ParallelExecution parallelExecution = new ParallelExecution(); + + // API testing settings + private ApiConfig api = new ApiConfig(); + + // UI testing settings + private UiConfig ui = new UiConfig(); + + public static class ParallelExecution { + private boolean enabled = true; + private int threadPoolSize = 4; + private int dataProviderThreadCount = 4; + + // getters and setters + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public int getThreadPoolSize() { return threadPoolSize; } + public void setThreadPoolSize(int threadPoolSize) { this.threadPoolSize = threadPoolSize; } + public int getDataProviderThreadCount() { return dataProviderThreadCount; } + public void setDataProviderThreadCount(int dataProviderThreadCount) { this.dataProviderThreadCount = dataProviderThreadCount; } + } + + public static class ApiConfig { + private int connectionTimeout = 30000; + private int socketTimeout = 30000; + private int maxRetries = 3; + private boolean logRequestResponse = false; + + // getters and setters + public int getConnectionTimeout() { return connectionTimeout; } + public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } + public int getSocketTimeout() { return socketTimeout; } + public void setSocketTimeout(int socketTimeout) { this.socketTimeout = socketTimeout; } + public int getMaxRetries() { return maxRetries; } + public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } + public boolean isLogRequestResponse() { return logRequestResponse; } + public void setLogRequestResponse(boolean logRequestResponse) { this.logRequestResponse = logRequestResponse; } + } + + public static class UiConfig { + private boolean headless = false; + private int implicitWait = 10; + private int pageLoadTimeout = 30; + private String windowSize = "1920x1080"; + private boolean enableVideoRecording = false; + + // getters and setters + public boolean isHeadless() { return headless; } + public void setHeadless(boolean headless) { this.headless = headless; } + public int getImplicitWait() { return implicitWait; } + public void setImplicitWait(int implicitWait) { this.implicitWait = implicitWait; } + public int getPageLoadTimeout() { return pageLoadTimeout; } + public void setPageLoadTimeout(int pageLoadTimeout) { this.pageLoadTimeout = pageLoadTimeout; } + public String getWindowSize() { return windowSize; } + public void setWindowSize(String windowSize) { this.windowSize = windowSize; } + public boolean isEnableVideoRecording() { return enableVideoRecording; } + public void setEnableVideoRecording(boolean enableVideoRecording) { this.enableVideoRecording = enableVideoRecording; } + } + + // Main getters and setters + public int getMaxRetries() { return maxRetries; } + public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } + public int getThreadCount() { return threadCount; } + public void setThreadCount(int threadCount) { this.threadCount = threadCount; } + public int getTimeoutSeconds() { return timeoutSeconds; } + public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } + public boolean isTakeScreenshotOnFailure() { return takeScreenshotOnFailure; } + public void setTakeScreenshotOnFailure(boolean takeScreenshotOnFailure) { this.takeScreenshotOnFailure = takeScreenshotOnFailure; } + public boolean isEnableDetailedReporting() { return enableDetailedReporting; } + public void setEnableDetailedReporting(boolean enableDetailedReporting) { this.enableDetailedReporting = enableDetailedReporting; } + public String getDefaultBrowser() { return defaultBrowser; } + public void setDefaultBrowser(String defaultBrowser) { this.defaultBrowser = defaultBrowser; } + public ParallelExecution getParallelExecution() { return parallelExecution; } + public void setParallelExecution(ParallelExecution parallelExecution) { this.parallelExecution = parallelExecution; } + public ApiConfig getApi() { return api; } + public void setApi(ApiConfig api) { this.api = api; } + public UiConfig getUi() { return ui; } + public void setUi(UiConfig ui) { this.ui = ui; } +} diff --git a/common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java b/common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java new file mode 100644 index 0000000..c0e9418 --- /dev/null +++ b/common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java @@ -0,0 +1,53 @@ +package com.cmccarthy.common.exception; + +public class TestFrameworkException extends RuntimeException { + + private final String testName; + private final String errorType; + + public TestFrameworkException(String message) { + super(message); + this.testName = null; + this.errorType = "GENERAL"; + } + + public TestFrameworkException(String message, Throwable cause) { + super(message, cause); + this.testName = null; + this.errorType = "GENERAL"; + } + + public TestFrameworkException(String message, String testName, String errorType) { + super(message); + this.testName = testName; + this.errorType = errorType; + } + + public TestFrameworkException(String message, Throwable cause, String testName, String errorType) { + super(message, cause); + this.testName = testName; + this.errorType = errorType; + } + + public String getTestName() { + return testName; + } + + public String getErrorType() { + return errorType; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("TestFrameworkException: "); + if (testName != null) { + sb.append("[Test: ").append(testName).append("] "); + } + if (errorType != null) { + sb.append("[Type: ").append(errorType).append("] "); + } + sb.append(getMessage()); + return sb.toString(); + } +} diff --git a/common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java b/common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java new file mode 100644 index 0000000..20cece5 --- /dev/null +++ b/common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java @@ -0,0 +1,55 @@ +package com.cmccarthy.common.listeners; + +import org.testng.IExecutionListener; +import org.testng.ITestListener; +import org.testng.ITestResult; + +public class TestExecutionListener implements IExecutionListener, ITestListener { + + @Override + public void onExecutionStart() { + System.out.println("========================================"); + System.out.println("Starting Test Execution"); + System.out.println("Thread Count: " + Thread.activeCount()); + System.out.println("========================================"); + } + + @Override + public void onExecutionFinish() { + System.out.println("========================================"); + System.out.println("Test Execution Completed"); + System.out.println("========================================"); + } + + @Override + public void onTestStart(ITestResult result) { + String testName = result.getMethod().getMethodName(); + String className = result.getTestClass().getName(); + System.out.printf("[%s] Starting test: %s.%s%n", + Thread.currentThread().getName(), className, testName); + } + + @Override + public void onTestSuccess(ITestResult result) { + String testName = result.getMethod().getMethodName(); + long duration = result.getEndMillis() - result.getStartMillis(); + System.out.printf("[%s] ✅ PASSED: %s (%dms)%n", + Thread.currentThread().getName(), testName, duration); + } + + @Override + public void onTestFailure(ITestResult result) { + String testName = result.getMethod().getMethodName(); + long duration = result.getEndMillis() - result.getStartMillis(); + System.out.printf("[%s] ❌ FAILED: %s (%dms) - %s%n", + Thread.currentThread().getName(), testName, duration, + result.getThrowable().getMessage()); + } + + @Override + public void onTestSkipped(ITestResult result) { + String testName = result.getMethod().getMethodName(); + System.out.printf("[%s] ⏭️ SKIPPED: %s%n", + Thread.currentThread().getName(), testName); + } +} diff --git a/common/src/main/java/com/cmccarthy/common/service/TestDataManager.java b/common/src/main/java/com/cmccarthy/common/service/TestDataManager.java new file mode 100644 index 0000000..5c89c69 --- /dev/null +++ b/common/src/main/java/com/cmccarthy/common/service/TestDataManager.java @@ -0,0 +1,95 @@ +package com.cmccarthy.common.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class TestDataManager { + + private final ObjectMapper jsonMapper = new ObjectMapper(); + private final Map testDataCache = new ConcurrentHashMap<>(); + private final ThreadLocal> testContextData = new ThreadLocal<>(); + + public void initializeTestContext() { + testContextData.set(new HashMap<>()); + } + + public void clearTestContext() { + testContextData.remove(); + } + + public void setContextValue(String key, Object value) { + Map context = testContextData.get(); + if (context == null) { + initializeTestContext(); + context = testContextData.get(); + } + context.put(key, value); + } + + public T getContextValue(String key, Class type) { + Map context = testContextData.get(); + if (context == null) { + return null; + } + Object value = context.get(key); + return type.cast(value); + } + + public Object getContextValue(String key) { + Map context = testContextData.get(); + if (context == null) { + return null; + } + return context.get(key); + } + + public Map getAllContextValues() { + Map context = testContextData.get(); + return context != null ? new HashMap<>(context) : new HashMap<>(); + } + + @SuppressWarnings("unchecked") + public T loadTestData(String resourcePath, Class type) { + String cacheKey = resourcePath + "_" + type.getName(); + + return (T) testDataCache.computeIfAbsent(cacheKey, key -> { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath)) { + if (inputStream == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + + return jsonMapper.readValue(inputStream, type); + } catch (IOException e) { + throw new RuntimeException("Failed to load test data from: " + resourcePath, e); + } + }); + } + + @SuppressWarnings("unchecked") + public Map loadTestDataAsMap(String resourcePath) { + return loadTestData(resourcePath, Map.class); + } + + public void clearCache() { + testDataCache.clear(); + } + + public String generateUniqueId() { + return "test_" + System.currentTimeMillis() + "_" + Thread.currentThread().getId(); + } + + public String generateTestEmail() { + return "test_" + System.currentTimeMillis() + "@example.com"; + } + + public String generateTestUsername() { + return "user_" + System.currentTimeMillis(); + } +} From aacb02a62db83ffff4e34ee0da71894e031c2dbd Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 14:51:45 +1000 Subject: [PATCH 03/28] init commit --- .github/workflows/enhanced-ci.yml | 6 +- .github/workflows/run.yml | 141 ++++++++++++++++++++++++------ 2 files changed, 114 insertions(+), 33 deletions(-) diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml index 16d7710..38baf8c 100644 --- a/.github/workflows/enhanced-ci.yml +++ b/.github/workflows/enhanced-ci.yml @@ -2,11 +2,9 @@ name: Enhanced CI/CD Pipeline on: push: - branches: [ master, develop ] + branches: [ feature/wip ] pull_request: - branches: [ master ] - schedule: - - cron: '0 2 * * *' # Daily at 2 AM + branches: [ main, develop ] env: JAVA_VERSION: '17' diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index d55216e..727265e 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -1,38 +1,121 @@ -name: run +name: Enhanced CI/CD Pipeline on: - pull_request: - branches: [ "master" ] push: - branches: [ "feature/WebDriverManager" ] + branches: [ feature/wip ] + pull_request: + branches: [ main, develop ] + +env: + JAVA_VERSION: '17' + MAVEN_OPTS: '-Xmx2048m' jobs: - build: + test: + name: Run Tests + runs-on: ubuntu-latest + strategy: + matrix: + module: [weather, wikipedia] + browser: [chrome, firefox] + fail-fast: false + max-parallel: 4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Setup Chrome + if: matrix.browser == 'chrome' + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + - name: Setup Firefox + if: matrix.browser == 'firefox' + uses: browser-actions/setup-firefox@v1 + with: + firefox-version: latest + + - name: Compile project + run: mvn clean compile test-compile + + - name: Run ${{ matrix.module }} tests with ${{ matrix.browser }} + run: | + cd ${{ matrix.module }} + mvn test -Dbrowser=${{ matrix.browser }} -Dtest.ui.headless=true -Dspring.profiles.active=headless-github + env: + BROWSER: ${{ matrix.browser }} + HEADLESS: true + + - name: Generate Allure Report + if: always() + run: | + cd ${{ matrix.module }} + mvn allure:report + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.module }}-${{ matrix.browser }} + path: | + ${{ matrix.module }}/target/allure-results/ + ${{ matrix.module }}/target/cucumber/ + ${{ matrix.module }}/logs/ + retention-days: 30 + + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots-${{ matrix.module }}-${{ matrix.browser }} + path: ${{ matrix.module }}/screenshots/ + retention-days: 7 + + security-scan: + name: Security Scan runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Snyk security scan + uses: snyk/actions/maven@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high + publish-reports: + name: Publish Test Reports + needs: test + runs-on: ubuntu-latest + if: always() + steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - name: Remove Chrome - run: sudo apt purge google-chrome-stable - - name: Remove default Chromium - run: sudo apt purge chromium-browser - - - name: Install Google Chrome # Using shell script to install Google Chrome - run: | - chmod +x ./.github/scripts/InstallChrome.sh - ./.github/scripts/InstallChrome.sh - - - run: | - export DISPLAY=:99 - chromedriver --url-base=/wd/hub & - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional - - - name: Test - run: mvn clean install -DactiveProfile=headless-github \ No newline at end of file + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Publish Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: Test Results + path: '**/target/cucumber/*.json' + reporter: java-junit + fail-on-error: false \ No newline at end of file From bf394f9d3cb57a4d314ec0826f77a64c5eeccdae Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 14:52:35 +1000 Subject: [PATCH 04/28] init commit --- .github/workflows/run.yml | 141 ++++++++------------------------------ 1 file changed, 29 insertions(+), 112 deletions(-) diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 727265e..d55216e 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -1,121 +1,38 @@ -name: Enhanced CI/CD Pipeline +name: run on: - push: - branches: [ feature/wip ] pull_request: - branches: [ main, develop ] - -env: - JAVA_VERSION: '17' - MAVEN_OPTS: '-Xmx2048m' + branches: [ "master" ] + push: + branches: [ "feature/WebDriverManager" ] jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - strategy: - matrix: - module: [weather, wikipedia] - browser: [chrome, firefox] - fail-fast: false - max-parallel: 4 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: 'temurin' - cache: maven - - - name: Cache Maven dependencies - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Setup Chrome - if: matrix.browser == 'chrome' - uses: browser-actions/setup-chrome@v1 - with: - chrome-version: stable + build: - - name: Setup Firefox - if: matrix.browser == 'firefox' - uses: browser-actions/setup-firefox@v1 - with: - firefox-version: latest - - - name: Compile project - run: mvn clean compile test-compile - - - name: Run ${{ matrix.module }} tests with ${{ matrix.browser }} - run: | - cd ${{ matrix.module }} - mvn test -Dbrowser=${{ matrix.browser }} -Dtest.ui.headless=true -Dspring.profiles.active=headless-github - env: - BROWSER: ${{ matrix.browser }} - HEADLESS: true - - - name: Generate Allure Report - if: always() - run: | - cd ${{ matrix.module }} - mvn allure:report - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-${{ matrix.module }}-${{ matrix.browser }} - path: | - ${{ matrix.module }}/target/allure-results/ - ${{ matrix.module }}/target/cucumber/ - ${{ matrix.module }}/logs/ - retention-days: 30 - - - name: Upload screenshots on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: screenshots-${{ matrix.module }}-${{ matrix.browser }} - path: ${{ matrix.module }}/screenshots/ - retention-days: 7 - - security-scan: - name: Security Scan runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run Snyk security scan - uses: snyk/actions/maven@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --severity-threshold=high - publish-reports: - name: Publish Test Reports - needs: test - runs-on: ubuntu-latest - if: always() - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - - - name: Publish Test Report - uses: dorny/test-reporter@v1 - if: always() - with: - name: Test Results - path: '**/target/cucumber/*.json' - reporter: java-junit - fail-on-error: false \ No newline at end of file + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Remove Chrome + run: sudo apt purge google-chrome-stable + - name: Remove default Chromium + run: sudo apt purge chromium-browser + + - name: Install Google Chrome # Using shell script to install Google Chrome + run: | + chmod +x ./.github/scripts/InstallChrome.sh + ./.github/scripts/InstallChrome.sh + + - run: | + export DISPLAY=:99 + chromedriver --url-base=/wd/hub & + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional + + - name: Test + run: mvn clean install -DactiveProfile=headless-github \ No newline at end of file From 3c6b26e928bcc9ba014bc9badf0e1b06e7dcd889 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 14:54:29 +1000 Subject: [PATCH 05/28] init commit --- .github/workflows/enhanced-ci.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml index 38baf8c..27c4a7d 100644 --- a/.github/workflows/enhanced-ci.yml +++ b/.github/workflows/enhanced-ci.yml @@ -87,20 +87,6 @@ jobs: path: ${{ matrix.module }}/screenshots/ retention-days: 7 - security-scan: - name: Security Scan - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run Snyk security scan - uses: snyk/actions/maven@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --severity-threshold=high - publish-reports: name: Publish Test Reports needs: test @@ -119,19 +105,3 @@ jobs: path: '**/target/cucumber/*.json' reporter: java-junit fail-on-error: false - - notification: - name: Notify Results - needs: [test, security-scan] - runs-on: ubuntu-latest - if: always() - - steps: - - name: Notify Slack - if: always() - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - channel: '#testing' - webhook_url: ${{ secrets.SLACK_WEBHOOK }} - fields: repo,message,commit,author,action,eventName,ref,workflow From 9a691b877ed0f307805e700a5160df7d64243051 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 14:58:14 +1000 Subject: [PATCH 06/28] init commit --- .github/workflows/enhanced-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml index 27c4a7d..7350a79 100644 --- a/.github/workflows/enhanced-ci.yml +++ b/.github/workflows/enhanced-ci.yml @@ -2,9 +2,9 @@ name: Enhanced CI/CD Pipeline on: push: - branches: [ feature/wip ] + branches: [ "feature/wip" ] pull_request: - branches: [ main, develop ] + branches: [ "main", "develop" ] env: JAVA_VERSION: '17' From c5087e2492d19d9f5bc40a35bb6ff261ea416966 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 15:01:28 +1000 Subject: [PATCH 07/28] init commit --- .github/workflows/enhanced-ci.yml | 107 ------------------------------ .github/workflows/run.yml | 2 +- 2 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 .github/workflows/enhanced-ci.yml diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml deleted file mode 100644 index 7350a79..0000000 --- a/.github/workflows/enhanced-ci.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Enhanced CI/CD Pipeline - -on: - push: - branches: [ "feature/wip" ] - pull_request: - branches: [ "main", "develop" ] - -env: - JAVA_VERSION: '17' - MAVEN_OPTS: '-Xmx2048m' - -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - strategy: - matrix: - module: [weather, wikipedia] - browser: [chrome, firefox] - fail-fast: false - max-parallel: 4 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: 'temurin' - cache: maven - - - name: Cache Maven dependencies - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Setup Chrome - if: matrix.browser == 'chrome' - uses: browser-actions/setup-chrome@v1 - with: - chrome-version: stable - - - name: Setup Firefox - if: matrix.browser == 'firefox' - uses: browser-actions/setup-firefox@v1 - with: - firefox-version: latest - - - name: Compile project - run: mvn clean compile test-compile - - - name: Run ${{ matrix.module }} tests with ${{ matrix.browser }} - run: | - cd ${{ matrix.module }} - mvn test -Dbrowser=${{ matrix.browser }} -Dtest.ui.headless=true -Dspring.profiles.active=headless-github - env: - BROWSER: ${{ matrix.browser }} - HEADLESS: true - - - name: Generate Allure Report - if: always() - run: | - cd ${{ matrix.module }} - mvn allure:report - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-${{ matrix.module }}-${{ matrix.browser }} - path: | - ${{ matrix.module }}/target/allure-results/ - ${{ matrix.module }}/target/cucumber/ - ${{ matrix.module }}/logs/ - retention-days: 30 - - - name: Upload screenshots on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: screenshots-${{ matrix.module }}-${{ matrix.browser }} - path: ${{ matrix.module }}/screenshots/ - retention-days: 7 - - publish-reports: - name: Publish Test Reports - needs: test - runs-on: ubuntu-latest - if: always() - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - - - name: Publish Test Report - uses: dorny/test-reporter@v1 - if: always() - with: - name: Test Results - path: '**/target/cucumber/*.json' - reporter: java-junit - fail-on-error: false diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index d55216e..51ba736 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [ "master" ] push: - branches: [ "feature/WebDriverManager" ] + branches: [ "feature/wip" ] jobs: build: From 44bda816b4833b4a7856ec171591fcd5812d3b57 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 15:03:19 +1000 Subject: [PATCH 08/28] init commit --- .github/workflows/enhanced-ci.yml | 105 ++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/enhanced-ci.yml diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml new file mode 100644 index 0000000..2cae04e --- /dev/null +++ b/.github/workflows/enhanced-ci.yml @@ -0,0 +1,105 @@ +name: Enhanced CI/CD Pipeline + +on: + pull_request: + branches: [ "main", "develop" ] + +env: + JAVA_VERSION: '17' + MAVEN_OPTS: '-Xmx2048m' + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + strategy: + matrix: + module: [weather, wikipedia] + browser: [chrome, firefox] + fail-fast: false + max-parallel: 4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Setup Chrome + if: matrix.browser == 'chrome' + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + + - name: Setup Firefox + if: matrix.browser == 'firefox' + uses: browser-actions/setup-firefox@v1 + with: + firefox-version: latest + + - name: Compile project + run: mvn clean compile test-compile + + - name: Run ${{ matrix.module }} tests with ${{ matrix.browser }} + run: | + cd ${{ matrix.module }} + mvn test -Dbrowser=${{ matrix.browser }} -Dtest.ui.headless=true -Dspring.profiles.active=headless-github + env: + BROWSER: ${{ matrix.browser }} + HEADLESS: true + + - name: Generate Allure Report + if: always() + run: | + cd ${{ matrix.module }} + mvn allure:report + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.module }}-${{ matrix.browser }} + path: | + ${{ matrix.module }}/target/allure-results/ + ${{ matrix.module }}/target/cucumber/ + ${{ matrix.module }}/logs/ + retention-days: 30 + + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots-${{ matrix.module }}-${{ matrix.browser }} + path: ${{ matrix.module }}/screenshots/ + retention-days: 7 + + publish-reports: + name: Publish Test Reports + needs: test + runs-on: ubuntu-latest + if: always() + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Publish Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: Test Results + path: '**/target/cucumber/*.json' + reporter: java-junit + fail-on-error: false From 6bd46821f768cb32c9199dd9ef109c300f8223ca Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 15:10:41 +1000 Subject: [PATCH 09/28] init commit --- .../com/cmccarthy/ui/utils/DriverManager.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index 407347e..b5a0bbb 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -26,6 +26,9 @@ import java.io.IOException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.util.Arrays; import java.util.HashMap; @@ -36,6 +39,7 @@ public class DriverManager { private static final ThreadLocal driverThreadLocal = new ThreadLocal<>(); + private static final ThreadLocal userDataDirThreadLocal = new ThreadLocal<>(); private final Logger log = LoggerFactory.getLogger(DriverManager.class); @Autowired @@ -125,12 +129,33 @@ private void setRemoteDriver(URL hubUrl) { private ChromeOptions getChromeOptions() { ChromeOptions options = new ChromeOptions(); + + // Essential arguments for stability and parallel execution options.addArguments("--disable-logging"); options.addArguments("--no-sandbox"); options.addArguments("--disable-dev-shm-usage"); options.addArguments("--disable-gpu"); options.addArguments("--disable-extensions"); options.addArguments("--start-maximized"); + options.addArguments("--disable-web-security"); + options.addArguments("--allow-running-insecure-content"); + options.addArguments("--disable-features=VizDisplayCompositor"); + options.addArguments("--remote-debugging-port=0"); // Use random available port + options.addArguments("--disable-background-timer-throttling"); + options.addArguments("--disable-backgrounding-occluded-windows"); + options.addArguments("--disable-renderer-backgrounding"); + + // Create unique user data directory for parallel execution + String uniqueUserDataDir = System.getProperty("java.io.tmpdir") + + "chrome_user_data_" + Thread.currentThread().getId() + "_" + System.currentTimeMillis(); + options.addArguments("--user-data-dir=" + uniqueUserDataDir); + + // Store the user data directory path for cleanup later + userDataDirThreadLocal.set(uniqueUserDataDir); + + // Prevent Chrome from creating crash dumps + options.addArguments("--disable-crash-reporter"); + options.addArguments("--disable-in-process-stack-traces"); if (isHeadlessMode()) { options.addArguments("--headless=new"); @@ -147,8 +172,13 @@ private ChromeOptions getChromeOptions() { Map prefs = new HashMap<>(); prefs.put("profile.default_content_setting_values.notifications", 2); prefs.put("profile.default_content_settings.popups", 0); + prefs.put("profile.managed_default_content_settings.images", 2); // Block images for faster loading options.setExperimentalOption("prefs", prefs); + // Additional performance settings + options.setExperimentalOption("useAutomationExtension", false); + options.addArguments("--disable-blink-features=AutomationControlled"); + return options; } @@ -253,11 +283,51 @@ public void quitDriver() { } catch (Exception e) { log.warn("Error while quitting WebDriver: {}", e.getMessage()); } finally { + // Clean up temporary user data directory + cleanupUserDataDirectory(); + + // Remove ThreadLocal references driverThreadLocal.remove(); + userDataDirThreadLocal.remove(); driverWait.getDriverWaitThreadLocal().remove(); } } } + + /** + * Clean up temporary Chrome user data directory + */ + private void cleanupUserDataDirectory() { + String userDataDir = userDataDirThreadLocal.get(); + if (userDataDir != null) { + try { + Path userDataPath = Paths.get(userDataDir); + if (Files.exists(userDataPath)) { + deleteDirectoryRecursively(userDataPath); + log.debug("Cleaned up Chrome user data directory: {}", userDataDir); + } + } catch (Exception e) { + log.warn("Failed to clean up Chrome user data directory {}: {}", userDataDir, e.getMessage()); + } + } + } + + /** + * Recursively delete directory and its contents + */ + private void deleteDirectoryRecursively(Path path) throws IOException { + if (Files.exists(path)) { + Files.walk(path) + .sorted((a, b) -> b.compareTo(a)) // Delete files first, then directories + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + log.warn("Failed to delete: {}", p.toString()); + } + }); + } + } /** * Download driver binaries if needed (placeholder for future enhancement) From 2832cfc394c578a68d69f28d993b104118dd0585 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 15:16:37 +1000 Subject: [PATCH 10/28] init commit --- .../com/cmccarthy/ui/utils/DriverManager.java | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index b5a0bbb..0464f01 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -145,20 +145,77 @@ private ChromeOptions getChromeOptions() { options.addArguments("--disable-backgrounding-occluded-windows"); options.addArguments("--disable-renderer-backgrounding"); - // Create unique user data directory for parallel execution - String uniqueUserDataDir = System.getProperty("java.io.tmpdir") + - "chrome_user_data_" + Thread.currentThread().getId() + "_" + System.currentTimeMillis(); + // GitHub Actions / CI-specific arguments + options.addArguments("--disable-software-rasterizer"); + options.addArguments("--disable-background-networking"); + options.addArguments("--disable-default-apps"); + options.addArguments("--disable-sync"); + options.addArguments("--metrics-recording-only"); + options.addArguments("--no-first-run"); + options.addArguments("--safebrowsing-disable-auto-update"); + options.addArguments("--disable-component-update"); + options.addArguments("--disable-domain-reliability"); + + // Detect CI environment and add additional arguments + boolean isCI = isRunningInCI(); + if (isCI) { + log.info("Detected CI environment, applying additional Chrome arguments"); + options.addArguments("--disable-features=MediaRouter"); + options.addArguments("--disable-speech-api"); + options.addArguments("--disable-file-system"); + options.addArguments("--disable-presentation-api"); + options.addArguments("--disable-permissions-api"); + options.addArguments("--disable-new-profile-management"); + options.addArguments("--disable-profile-shortcut-manager"); + } + + // Create unique user data directory with proper path handling + String tempDir = System.getProperty("java.io.tmpdir"); + if (tempDir == null || tempDir.isEmpty()) { + tempDir = "/tmp"; // Fallback for Unix-like systems including GitHub Actions + } + + // Ensure the temp directory ends with a separator + if (!tempDir.endsWith(System.getProperty("file.separator"))) { + tempDir += System.getProperty("file.separator"); + } + + String uniqueUserDataDir = tempDir + "chrome_user_data_" + + Thread.currentThread().getId() + "_" + System.currentTimeMillis(); + + // Create the directory proactively to ensure it exists + try { + Path userDataPath = Paths.get(uniqueUserDataDir); + Files.createDirectories(userDataPath); + log.debug("Created Chrome user data directory: {}", uniqueUserDataDir); + } catch (IOException e) { + log.warn("Failed to create Chrome user data directory: {}", e.getMessage()); + // Fallback to a simpler path if creation fails + uniqueUserDataDir = tempDir + "chrome_" + System.currentTimeMillis(); + } + options.addArguments("--user-data-dir=" + uniqueUserDataDir); // Store the user data directory path for cleanup later userDataDirThreadLocal.set(uniqueUserDataDir); + // Additional CI/GitHub Actions specific arguments + options.addArguments("--disable-features=TranslateUI"); + options.addArguments("--disable-ipc-flooding-protection"); + options.addArguments("--disable-hang-monitor"); + options.addArguments("--disable-prompt-on-repost"); + options.addArguments("--disable-client-side-phishing-detection"); + // Prevent Chrome from creating crash dumps options.addArguments("--disable-crash-reporter"); options.addArguments("--disable-in-process-stack-traces"); + options.addArguments("--disable-logging"); + options.addArguments("--disable-dev-tools"); if (isHeadlessMode()) { options.addArguments("--headless=new"); + // Additional headless-specific arguments for CI + options.addArguments("--virtual-time-budget=1000"); } if (testConfiguration != null) { @@ -173,6 +230,8 @@ private ChromeOptions getChromeOptions() { prefs.put("profile.default_content_setting_values.notifications", 2); prefs.put("profile.default_content_settings.popups", 0); prefs.put("profile.managed_default_content_settings.images", 2); // Block images for faster loading + prefs.put("profile.default_content_setting_values.geolocation", 2); + prefs.put("profile.default_content_setting_values.media_stream", 2); options.setExperimentalOption("prefs", prefs); // Additional performance settings @@ -251,6 +310,17 @@ private boolean isHeadlessMode() { Boolean.parseBoolean(System.getenv("HEADLESS")) || Arrays.toString(this.environment.getActiveProfiles()).contains("headless"); } + + /** + * Detect if running in CI environment (GitHub Actions, Jenkins, etc.) + */ + private boolean isRunningInCI() { + return System.getenv("CI") != null || + System.getenv("GITHUB_ACTIONS") != null || + System.getenv("JENKINS_URL") != null || + System.getenv("BUILD_NUMBER") != null || + System.getProperty("ci") != null; + } public WebDriver getDriver() { return driverThreadLocal.get(); From c0e043697a72602935b43990ff5d255b564d8794 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 15:29:47 +1000 Subject: [PATCH 11/28] init commit --- .../exception/TestFrameworkException.java | 53 ----------- .../listeners/TestExecutionListener.java | 55 ----------- .../common/service/TestDataManager.java | 95 ------------------- 3 files changed, 203 deletions(-) delete mode 100644 common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java delete mode 100644 common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java delete mode 100644 common/src/main/java/com/cmccarthy/common/service/TestDataManager.java diff --git a/common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java b/common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java deleted file mode 100644 index c0e9418..0000000 --- a/common/src/main/java/com/cmccarthy/common/exception/TestFrameworkException.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.cmccarthy.common.exception; - -public class TestFrameworkException extends RuntimeException { - - private final String testName; - private final String errorType; - - public TestFrameworkException(String message) { - super(message); - this.testName = null; - this.errorType = "GENERAL"; - } - - public TestFrameworkException(String message, Throwable cause) { - super(message, cause); - this.testName = null; - this.errorType = "GENERAL"; - } - - public TestFrameworkException(String message, String testName, String errorType) { - super(message); - this.testName = testName; - this.errorType = errorType; - } - - public TestFrameworkException(String message, Throwable cause, String testName, String errorType) { - super(message, cause); - this.testName = testName; - this.errorType = errorType; - } - - public String getTestName() { - return testName; - } - - public String getErrorType() { - return errorType; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("TestFrameworkException: "); - if (testName != null) { - sb.append("[Test: ").append(testName).append("] "); - } - if (errorType != null) { - sb.append("[Type: ").append(errorType).append("] "); - } - sb.append(getMessage()); - return sb.toString(); - } -} diff --git a/common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java b/common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java deleted file mode 100644 index 20cece5..0000000 --- a/common/src/main/java/com/cmccarthy/common/listeners/TestExecutionListener.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.cmccarthy.common.listeners; - -import org.testng.IExecutionListener; -import org.testng.ITestListener; -import org.testng.ITestResult; - -public class TestExecutionListener implements IExecutionListener, ITestListener { - - @Override - public void onExecutionStart() { - System.out.println("========================================"); - System.out.println("Starting Test Execution"); - System.out.println("Thread Count: " + Thread.activeCount()); - System.out.println("========================================"); - } - - @Override - public void onExecutionFinish() { - System.out.println("========================================"); - System.out.println("Test Execution Completed"); - System.out.println("========================================"); - } - - @Override - public void onTestStart(ITestResult result) { - String testName = result.getMethod().getMethodName(); - String className = result.getTestClass().getName(); - System.out.printf("[%s] Starting test: %s.%s%n", - Thread.currentThread().getName(), className, testName); - } - - @Override - public void onTestSuccess(ITestResult result) { - String testName = result.getMethod().getMethodName(); - long duration = result.getEndMillis() - result.getStartMillis(); - System.out.printf("[%s] ✅ PASSED: %s (%dms)%n", - Thread.currentThread().getName(), testName, duration); - } - - @Override - public void onTestFailure(ITestResult result) { - String testName = result.getMethod().getMethodName(); - long duration = result.getEndMillis() - result.getStartMillis(); - System.out.printf("[%s] ❌ FAILED: %s (%dms) - %s%n", - Thread.currentThread().getName(), testName, duration, - result.getThrowable().getMessage()); - } - - @Override - public void onTestSkipped(ITestResult result) { - String testName = result.getMethod().getMethodName(); - System.out.printf("[%s] ⏭️ SKIPPED: %s%n", - Thread.currentThread().getName(), testName); - } -} diff --git a/common/src/main/java/com/cmccarthy/common/service/TestDataManager.java b/common/src/main/java/com/cmccarthy/common/service/TestDataManager.java deleted file mode 100644 index 5c89c69..0000000 --- a/common/src/main/java/com/cmccarthy/common/service/TestDataManager.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.cmccarthy.common.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class TestDataManager { - - private final ObjectMapper jsonMapper = new ObjectMapper(); - private final Map testDataCache = new ConcurrentHashMap<>(); - private final ThreadLocal> testContextData = new ThreadLocal<>(); - - public void initializeTestContext() { - testContextData.set(new HashMap<>()); - } - - public void clearTestContext() { - testContextData.remove(); - } - - public void setContextValue(String key, Object value) { - Map context = testContextData.get(); - if (context == null) { - initializeTestContext(); - context = testContextData.get(); - } - context.put(key, value); - } - - public T getContextValue(String key, Class type) { - Map context = testContextData.get(); - if (context == null) { - return null; - } - Object value = context.get(key); - return type.cast(value); - } - - public Object getContextValue(String key) { - Map context = testContextData.get(); - if (context == null) { - return null; - } - return context.get(key); - } - - public Map getAllContextValues() { - Map context = testContextData.get(); - return context != null ? new HashMap<>(context) : new HashMap<>(); - } - - @SuppressWarnings("unchecked") - public T loadTestData(String resourcePath, Class type) { - String cacheKey = resourcePath + "_" + type.getName(); - - return (T) testDataCache.computeIfAbsent(cacheKey, key -> { - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath)) { - if (inputStream == null) { - throw new IllegalArgumentException("Resource not found: " + resourcePath); - } - - return jsonMapper.readValue(inputStream, type); - } catch (IOException e) { - throw new RuntimeException("Failed to load test data from: " + resourcePath, e); - } - }); - } - - @SuppressWarnings("unchecked") - public Map loadTestDataAsMap(String resourcePath) { - return loadTestData(resourcePath, Map.class); - } - - public void clearCache() { - testDataCache.clear(); - } - - public String generateUniqueId() { - return "test_" + System.currentTimeMillis() + "_" + Thread.currentThread().getId(); - } - - public String generateTestEmail() { - return "test_" + System.currentTimeMillis() + "@example.com"; - } - - public String generateTestUsername() { - return "user_" + System.currentTimeMillis(); - } -} From 73c14466b2fe1edc5d074fbd4b7df5faec491bda Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 15:45:35 +1000 Subject: [PATCH 12/28] init commit --- .../java/com/cmccarthy/ui/step/Hooks.java | 9 - .../com/cmccarthy/ui/utils/DriverManager.java | 369 ++++-------------- 2 files changed, 86 insertions(+), 292 deletions(-) diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java index ac9b43b..4326d6a 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java @@ -27,15 +27,6 @@ public class Hooks extends WikipediaAbstractTestDefinition { @Before public void beforeScenario(Scenario scenario) throws IOException { - synchronized (lock) { - if (!initialized) { - if (!driverManager.isDriverExisting()) { - driverManager.downloadDriver(); - } - initialized = true; - } - } - String filename = scenario.getName().replaceAll("\\s+", "_"); logManager.createNewLogger(filename); driverManager.createDriver(); diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index 0464f01..16fb029 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -4,6 +4,7 @@ import com.cmccarthy.common.utils.ApplicationProperties; import com.cmccarthy.common.utils.Constants; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; @@ -26,86 +27,126 @@ import java.io.IOException; import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Duration; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.NoSuchElementException; @Component public class DriverManager { private static final ThreadLocal driverThreadLocal = new ThreadLocal<>(); - private static final ThreadLocal userDataDirThreadLocal = new ThreadLocal<>(); private final Logger log = LoggerFactory.getLogger(DriverManager.class); @Autowired private ApplicationProperties applicationProperties; @Autowired + private TestConfiguration testConfig; + @Autowired private DriverWait driverWait; @Autowired private Environment environment; - @Autowired(required = false) - private TestConfiguration testConfiguration; public void createDriver() throws IOException { if (getDriver() == null) { - log.info("Creating WebDriver for browser: {}", applicationProperties.getBrowser()); - if (Arrays.toString(this.environment.getActiveProfiles()).contains("cloud-provider")) { setRemoteDriver(new URL(applicationProperties.getGridUrl())); } else { setLocalWebDriver(); } + getDriver().manage().deleteAllCookies();//useful for AJAX pages - configureDriver(); + // Set the window size if specified in the configuration + if (testConfig.getUi().getWindowSize() != null && !testConfig.getUi().getWindowSize().isEmpty()) { + String[] dimensions = testConfig.getUi().getWindowSize().split("x"); + if (dimensions.length == 2) { + try { + int width = Integer.parseInt(dimensions[0]); + int height = Integer.parseInt(dimensions[1]); + getDriver().manage().window().setSize(new Dimension(width, height)); + log.info("Set browser window size to: {}x{}", width, height); + } catch (NumberFormatException e) { + log.error("Failed to parse window size from configuration: {}", testConfig.getUi().getWindowSize()); + } + } + } } } public void setLocalWebDriver() throws IOException { String browser = applicationProperties.getBrowser(); - log.info("Creating local {} driver", browser); - - switch (browser.toLowerCase()) { - case "chrome" -> { - driverThreadLocal.set(new ChromeDriver(getChromeOptions())); + boolean isHeadless = testConfig.getUi().isHeadless(); + + switch (browser) { + case ("chrome") -> { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--disable-logging"); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + options.addArguments("--start-maximized"); + if (isHeadless) { + options.addArguments("--headless=new"); + } + driverThreadLocal.set(new ChromeDriver(options)); } - case "firefox" -> { - driverThreadLocal.set(new FirefoxDriver(getFirefoxOptions())); + case ("firefox") -> { + FirefoxOptions firefoxOptions = new FirefoxOptions(); + firefoxOptions.setCapability("marionette", true); + if (isHeadless) { + firefoxOptions.addArguments("--headless"); + } + driverThreadLocal.set(new FirefoxDriver(firefoxOptions)); } - case "ie" -> { + case ("ie") -> { InternetExplorerOptions capabilitiesIE = new InternetExplorerOptions(); capabilitiesIE.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true); driverThreadLocal.set(new InternetExplorerDriver(capabilitiesIE)); } - case "safari" -> { - driverThreadLocal.set(new SafariDriver(getSafariOptions())); + case ("safari") -> { + SafariOptions operaOptions = new SafariOptions(); + driverThreadLocal.set(new SafariDriver(operaOptions)); } - case "edge" -> { - driverThreadLocal.set(new EdgeDriver(getEdgeOptions())); + case ("edge") -> { + EdgeOptions edgeOptions = new EdgeOptions(); + if (isHeadless) { + edgeOptions.addArguments("--headless"); + } + driverThreadLocal.set(new EdgeDriver(edgeOptions)); } default -> throw new NoSuchElementException("Failed to create an instance of WebDriver for: " + browser); } - setupDriverWait(); + // Configure WebDriver timeouts + WebDriver driver = driverThreadLocal.get(); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(testConfig.getUi().getImplicitWait())); + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(testConfig.getUi().getPageLoadTimeout())); + + driverWait.getDriverWaitThreadLocal() + .set(new WebDriverWait(driver, + Duration.ofSeconds(testConfig.getTimeoutSeconds()), + Duration.ofMillis(Constants.pollingShort))); } private void setRemoteDriver(URL hubUrl) { + Capabilities capability; String browser = applicationProperties.getBrowser(); - log.info("Creating remote {} driver at {}", browser, hubUrl); + boolean isHeadless = testConfig.getUi().isHeadless(); - Capabilities capability; - switch (browser.toLowerCase()) { + switch (browser) { case "firefox" -> { - capability = getFirefoxOptions(); - driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); + FirefoxOptions options = new FirefoxOptions(); + if (isHeadless) { + options.addArguments("--headless"); + } + driverThreadLocal.set(new RemoteWebDriver(hubUrl, options)); } case "chrome" -> { - ChromeOptions options = getChromeOptions(); + ChromeOptions options = new ChromeOptions(); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + if (isHeadless) { + options.addArguments("--headless"); + } driverThreadLocal.set(new RemoteWebDriver(hubUrl, options)); } case "ie" -> { @@ -113,213 +154,29 @@ private void setRemoteDriver(URL hubUrl) { driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } case "safari" -> { - capability = getSafariOptions(); + capability = new SafariOptions(); driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } - case "edge" -> { - capability = getEdgeOptions(); - driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); + case ("edge") -> { + EdgeOptions options = new EdgeOptions(); + if (isHeadless) { + options.addArguments("--headless"); + } + driverThreadLocal.set(new RemoteWebDriver(hubUrl, options)); } default -> throw new NoSuchElementException("Failed to create an instance of RemoteWebDriver for: " + browser); } - setupDriverWait(); - } - - private ChromeOptions getChromeOptions() { - ChromeOptions options = new ChromeOptions(); - - // Essential arguments for stability and parallel execution - options.addArguments("--disable-logging"); - options.addArguments("--no-sandbox"); - options.addArguments("--disable-dev-shm-usage"); - options.addArguments("--disable-gpu"); - options.addArguments("--disable-extensions"); - options.addArguments("--start-maximized"); - options.addArguments("--disable-web-security"); - options.addArguments("--allow-running-insecure-content"); - options.addArguments("--disable-features=VizDisplayCompositor"); - options.addArguments("--remote-debugging-port=0"); // Use random available port - options.addArguments("--disable-background-timer-throttling"); - options.addArguments("--disable-backgrounding-occluded-windows"); - options.addArguments("--disable-renderer-backgrounding"); - - // GitHub Actions / CI-specific arguments - options.addArguments("--disable-software-rasterizer"); - options.addArguments("--disable-background-networking"); - options.addArguments("--disable-default-apps"); - options.addArguments("--disable-sync"); - options.addArguments("--metrics-recording-only"); - options.addArguments("--no-first-run"); - options.addArguments("--safebrowsing-disable-auto-update"); - options.addArguments("--disable-component-update"); - options.addArguments("--disable-domain-reliability"); - - // Detect CI environment and add additional arguments - boolean isCI = isRunningInCI(); - if (isCI) { - log.info("Detected CI environment, applying additional Chrome arguments"); - options.addArguments("--disable-features=MediaRouter"); - options.addArguments("--disable-speech-api"); - options.addArguments("--disable-file-system"); - options.addArguments("--disable-presentation-api"); - options.addArguments("--disable-permissions-api"); - options.addArguments("--disable-new-profile-management"); - options.addArguments("--disable-profile-shortcut-manager"); - } - - // Create unique user data directory with proper path handling - String tempDir = System.getProperty("java.io.tmpdir"); - if (tempDir == null || tempDir.isEmpty()) { - tempDir = "/tmp"; // Fallback for Unix-like systems including GitHub Actions - } - - // Ensure the temp directory ends with a separator - if (!tempDir.endsWith(System.getProperty("file.separator"))) { - tempDir += System.getProperty("file.separator"); - } - - String uniqueUserDataDir = tempDir + "chrome_user_data_" + - Thread.currentThread().getId() + "_" + System.currentTimeMillis(); - - // Create the directory proactively to ensure it exists - try { - Path userDataPath = Paths.get(uniqueUserDataDir); - Files.createDirectories(userDataPath); - log.debug("Created Chrome user data directory: {}", uniqueUserDataDir); - } catch (IOException e) { - log.warn("Failed to create Chrome user data directory: {}", e.getMessage()); - // Fallback to a simpler path if creation fails - uniqueUserDataDir = tempDir + "chrome_" + System.currentTimeMillis(); - } - - options.addArguments("--user-data-dir=" + uniqueUserDataDir); - - // Store the user data directory path for cleanup later - userDataDirThreadLocal.set(uniqueUserDataDir); - - // Additional CI/GitHub Actions specific arguments - options.addArguments("--disable-features=TranslateUI"); - options.addArguments("--disable-ipc-flooding-protection"); - options.addArguments("--disable-hang-monitor"); - options.addArguments("--disable-prompt-on-repost"); - options.addArguments("--disable-client-side-phishing-detection"); - - // Prevent Chrome from creating crash dumps - options.addArguments("--disable-crash-reporter"); - options.addArguments("--disable-in-process-stack-traces"); - options.addArguments("--disable-logging"); - options.addArguments("--disable-dev-tools"); - - if (isHeadlessMode()) { - options.addArguments("--headless=new"); - // Additional headless-specific arguments for CI - options.addArguments("--virtual-time-budget=1000"); - } - - if (testConfiguration != null) { - String windowSize = testConfiguration.getUi().getWindowSize(); - if (windowSize != null && !windowSize.isEmpty()) { - options.addArguments("--window-size=" + windowSize); - } - } - - // Performance optimizations - Map prefs = new HashMap<>(); - prefs.put("profile.default_content_setting_values.notifications", 2); - prefs.put("profile.default_content_settings.popups", 0); - prefs.put("profile.managed_default_content_settings.images", 2); // Block images for faster loading - prefs.put("profile.default_content_setting_values.geolocation", 2); - prefs.put("profile.default_content_setting_values.media_stream", 2); - options.setExperimentalOption("prefs", prefs); - - // Additional performance settings - options.setExperimentalOption("useAutomationExtension", false); - options.addArguments("--disable-blink-features=AutomationControlled"); - - return options; - } - - private FirefoxOptions getFirefoxOptions() { - FirefoxOptions options = new FirefoxOptions(); - options.setCapability("marionette", true); + // Configure WebDriver timeouts + WebDriver driver = driverThreadLocal.get(); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(testConfig.getUi().getImplicitWait())); + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(testConfig.getUi().getPageLoadTimeout())); - if (isHeadlessMode()) { - options.addArguments("--headless"); - } - - return options; - } - - private EdgeOptions getEdgeOptions() { - EdgeOptions options = new EdgeOptions(); - options.addArguments("--disable-logging"); - options.addArguments("--no-sandbox"); - options.addArguments("--disable-dev-shm-usage"); - - if (isHeadlessMode()) { - options.addArguments("--headless"); - } - - return options; - } - - private SafariOptions getSafariOptions() { - return new SafariOptions(); - } - - private void configureDriver() { - WebDriver driver = getDriver(); - if (driver != null) { - // Configure timeouts - if (testConfiguration != null) { - TestConfiguration.UiConfig uiConfig = testConfiguration.getUi(); - driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(uiConfig.getImplicitWait())); - driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(uiConfig.getPageLoadTimeout())); - } else { - // Fallback to default values - driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(Constants.timeoutShort)); - driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(Constants.timeoutLong)); - } - - driver.manage().deleteAllCookies(); // useful for AJAX pages - - if (!isHeadlessMode()) { - driver.manage().window().maximize(); - } - - log.info("WebDriver configured successfully for browser: {}", applicationProperties.getBrowser()); - } - } - - private void setupDriverWait() { driverWait.getDriverWaitThreadLocal() - .set(new WebDriverWait(driverThreadLocal.get(), - Duration.ofSeconds(Constants.timeoutShort), - Duration.ofSeconds(Constants.pollingShort))); - } - - private boolean isHeadlessMode() { - if (testConfiguration != null) { - return testConfiguration.getUi().isHeadless(); - } - - // Check system property, environment variable, or active profiles as fallback - return Boolean.parseBoolean(System.getProperty("headless", "false")) || - Boolean.parseBoolean(System.getenv("HEADLESS")) || - Arrays.toString(this.environment.getActiveProfiles()).contains("headless"); - } - - /** - * Detect if running in CI environment (GitHub Actions, Jenkins, etc.) - */ - private boolean isRunningInCI() { - return System.getenv("CI") != null || - System.getenv("GITHUB_ACTIONS") != null || - System.getenv("JENKINS_URL") != null || - System.getenv("BUILD_NUMBER") != null || - System.getProperty("ci") != null; + .set(new WebDriverWait(driver, + Duration.ofSeconds(testConfig.getTimeoutSeconds()), + Duration.ofMillis(Constants.pollingShort))); } public WebDriver getDriver() { @@ -334,13 +191,6 @@ public JavascriptExecutor getJSExecutor() { return (JavascriptExecutor) getDriver(); } - /** - * Check if driver is existing and not null - */ - public boolean isDriverExisting() { - return getDriver() != null; - } - /** * Safely quit and clean up the driver */ @@ -353,57 +203,10 @@ public void quitDriver() { } catch (Exception e) { log.warn("Error while quitting WebDriver: {}", e.getMessage()); } finally { - // Clean up temporary user data directory - cleanupUserDataDirectory(); - // Remove ThreadLocal references driverThreadLocal.remove(); - userDataDirThreadLocal.remove(); driverWait.getDriverWaitThreadLocal().remove(); } } } - - /** - * Clean up temporary Chrome user data directory - */ - private void cleanupUserDataDirectory() { - String userDataDir = userDataDirThreadLocal.get(); - if (userDataDir != null) { - try { - Path userDataPath = Paths.get(userDataDir); - if (Files.exists(userDataPath)) { - deleteDirectoryRecursively(userDataPath); - log.debug("Cleaned up Chrome user data directory: {}", userDataDir); - } - } catch (Exception e) { - log.warn("Failed to clean up Chrome user data directory {}: {}", userDataDir, e.getMessage()); - } - } - } - - /** - * Recursively delete directory and its contents - */ - private void deleteDirectoryRecursively(Path path) throws IOException { - if (Files.exists(path)) { - Files.walk(path) - .sorted((a, b) -> b.compareTo(a)) // Delete files first, then directories - .forEach(p -> { - try { - Files.deleteIfExists(p); - } catch (IOException e) { - log.warn("Failed to delete: {}", p.toString()); - } - }); - } - } - - /** - * Download driver binaries if needed (placeholder for future enhancement) - */ - public void downloadDriver() { - log.info("Driver download check - using WebDriverManager or system drivers"); - // Future enhancement: integrate with WebDriverManager for automatic driver downloads - } -} +} \ No newline at end of file From 68e96483f843ab67310988eb15f1bf0b4e96ff88 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 16:00:38 +1000 Subject: [PATCH 13/28] init commit --- .github/workflows/run.yml | 1 - common/src/main/resources/downloadDriver.sh | 51 --------------------- 2 files changed, 52 deletions(-) delete mode 100755 common/src/main/resources/downloadDriver.sh diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 51ba736..5738ce2 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -31,7 +31,6 @@ jobs: - run: | export DISPLAY=:99 - chromedriver --url-base=/wd/hub & sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional - name: Test diff --git a/common/src/main/resources/downloadDriver.sh b/common/src/main/resources/downloadDriver.sh deleted file mode 100755 index 0a6e850..0000000 --- a/common/src/main/resources/downloadDriver.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -set -x -export SCRIPT_DIR=../../../../wikipedia/src/test/resources -cd $SCRIPT_DIR && - -ROOT_DIR=$(pwd) -TEST_RESOURCES=$ROOT_DIR -FILE_EXTENSION="" - -echo $TEST_RESOURCES - -case "$OSTYPE" in -solaris*) echo "$OSTYPE not supported" ;; -darwin*) OS='mac64' ;; -linux*) OS='linux64' ;; -bsd*) echo "$OSTYPE not supported" ;; -msys*) OS='win32' FILE_EXTENSION=".exe" ;; -*) echo "unknown $OSTYPE" ;; -esac - -if [ ! -f "$TEST_RESOURCES/drivers/chromedriver"$FILE_EXTENSION ]; then - cd "$TEST_RESOURCES/drivers" && - mkdir "temp" && - cd "temp" && - curl -L -k --output driver.zip https://www.nuget.org/api/v2/package/Selenium.WebDriver.ChromeDriver/ --ssl-no-revoke && - unzip driver.zip && - cd driver/$OS && - cp "chromedriver$FILE_EXTENSION" "$TEST_RESOURCES/drivers" && - chmod +700 "$TEST_RESOURCES/drivers/chromedriver"$FILE_EXTENSION && - cd ../../../ && - rm -rf temp && - cd $ROOT_DIR -fi - -if [ ! -f "$TEST_RESOURCES/drivers/geckodriver"$FILE_EXTENSION ]; then - cd "$TEST_RESOURCES/drivers" && - mkdir "temp" && - cd "temp" && - curl -L -k --output driver.zip https://www.nuget.org/api/v2/package/Selenium.WebDriver.GeckoDriver/ --ssl-no-revoke && - unzip driver.zip && - cd driver/$OS && - cp "geckodriver$FILE_EXTENSION" "$TEST_RESOURCES/drivers" && - chmod +700 "$TEST_RESOURCES/drivers/geckodriver"$FILE_EXTENSION && - cd ../../../ && - rm -rf temp && - cd $ROOT_DIR -fi - -#uncomment to keep bash open -#! /bin/bash From 0bcac45d4589df32e250ec6a0d5e39c8fdd10839 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 16:09:24 +1000 Subject: [PATCH 14/28] init commit --- .../java/com/cmccarthy/common/config/TestConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java index f0158d1..d08f5f1 100644 --- a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java +++ b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java @@ -55,7 +55,7 @@ public static class ApiConfig { } public static class UiConfig { - private boolean headless = false; + private boolean headless = true; private int implicitWait = 10; private int pageLoadTimeout = 30; private String windowSize = "1920x1080"; From 8b6396e47213a7e9bbb6f1424e9bfe4e3e4fa900 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 16:29:22 +1000 Subject: [PATCH 15/28] init commit --- pom.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 41ce6ad..199ad73 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.0 + 3.5.3 @@ -43,7 +43,7 @@ org.springframework.boot spring-boot-dependencies - 3.5.0 + 3.5.3 pom import @@ -57,22 +57,22 @@ UTF-8 20250517 5.5.0 - 7.22.0 - 7.22.0 - 7.22.0 - 1.5.13 + 7.25.0 + 7.25.0 + 7.25.0 + 1.5.18 32.1.1 2.15.1 - 3.23.1 + 3.26.0 1.14.0 2.29.1 4.33.0 - 1.7.0 - 3.1.2 - 3.2.3 - 2.18.2 + 3.1.0 + 3.2.5 + 3.3.2 + 2.19.0 3.0.0 - 4.2.2 + 4.3.0 From 654074cf724217b681f78a4705ef0e5992d4f335 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 16:36:14 +1000 Subject: [PATCH 16/28] init commit --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 199ad73..0eff98f 100644 --- a/pom.xml +++ b/pom.xml @@ -68,8 +68,8 @@ 2.29.1 4.33.0 3.1.0 - 3.2.5 - 3.3.2 + 3.1.2 + 3.2.3 2.19.0 3.0.0 4.3.0 From 119299180c45beb8b10c3acb66bb705d7b117384 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 16:56:14 +1000 Subject: [PATCH 17/28] init commit --- README.md | 108 ++++++++++++++++++ .../common/config/TestConfiguration.java | 63 +++------- .../cmccarthy/common/service/RestService.java | 30 ++--- .../common/utils/ApplicationProperties.java | 17 +-- .../com/cmccarthy/common/utils/Constants.java | 4 - .../cmccarthy/common/utils/DateTimeUtil.java | 25 ++-- .../cmccarthy/api/service/WeatherService.java | 11 +- .../com/cmccarthy/ui/utils/DriverManager.java | 6 +- 8 files changed, 150 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index e69de29..fc56f02 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,108 @@ +# Spring Cucumber TestNG Parallel Test Harness + +A robust test automation framework built using Spring Boot, Cucumber, and TestNG for parallel test execution. This project demonstrates how to organize and execute automated tests efficiently in parallel across multiple modules. + +## 🚀 Features + +- **Multi-Module Architecture**: Separate modules for common functionality, weather tests, and Wikipedia tests +- **Parallel Execution**: Run tests concurrently using TestNG +- **Spring Boot Integration**: Leverage dependency injection and application properties +- **Comprehensive Reporting**: Allure and Extent reports for detailed test results +- **Logging**: Dedicated logs for each test scenario +- **Cross-Environment Support**: Multiple environment configurations (dev, uat, prod) + +## 📋 Requirements + +- Java 11 or higher +- Maven 3.6 or higher +- Git + +## 🔧 Setup & Installation + +1. Clone the repository: + ```bash + git clone https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness.git + cd spring-cucumber-testng-parallel-test-harness + ``` + +2. Install dependencies: + ```bash + mvn clean install -DskipTests + ``` + +## 🏃‍♂️ Running Tests + +### Running All Tests +```bash +mvn clean test +``` + +### Running Tests for a Specific Module +```bash +mvn clean test -pl weather +# or +mvn clean test -pl wikipedia +``` + +### Running with Specific Environment +```bash +mvn clean test -Dspring.profiles.active=dev +``` + +Available profiles: `dev`, `uat`, `prod`, `headless-github`, `cloud-provider` + +## 📊 Reporting + +After test execution, reports are available at: + +- **Allure Reports**: `/target/allure-results/` +- **Extent Reports**: `/target/cucumber/report.html` +- **TestNG Reports**: `/target/surefire-reports/index.html` + +To generate and open Allure reports: +```bash +mvn allure:serve +``` + +## 📁 Project Structure + +- **common**: Shared utilities, base classes, and configurations + - Configuration properties + - Common test utilities + - Shared step definitions + +- **weather**: Weather API testing module + - Feature files for weather API tests + - Step definitions for weather tests + - API client for weather services + +- **wikipedia**: Wikipedia functionality tests + - Feature files for Wikipedia tests + - Step definitions for Wikipedia interactions + - Page objects for Wikipedia pages + +## 🌐 Environment Configuration + +The framework supports multiple environments through Spring profiles: + +- **dev**: Development environment +- **uat**: User Acceptance Testing environment +- **prod**: Production environment +- **headless-github**: Headless browser configuration for CI/CD +- **cloud-provider**: Configuration for cloud-based test execution + +## 📝 Logging + +Test execution logs are stored in the `logs` directory of each module. Log files are named after the test scenario for easy debugging. + +## 🤝 Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 👥 Maintainers + +- [cmccarthyIrl](https://github.com/cmccarthyIrl) diff --git a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java index d08f5f1..6558699 100644 --- a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java +++ b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java @@ -1,12 +1,17 @@ package com.cmccarthy.common.config; +import lombok.Getter; +import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +@Setter +@Getter @Configuration @ConfigurationProperties(prefix = "test") public class TestConfiguration { + // Main getters and setters private int maxRetries = 3; private int threadCount = 4; private int timeoutSeconds = 30; @@ -23,74 +28,34 @@ public class TestConfiguration { // UI testing settings private UiConfig ui = new UiConfig(); + @Setter + @Getter public static class ParallelExecution { + // getters and setters private boolean enabled = true; private int threadPoolSize = 4; private int dataProviderThreadCount = 4; - - // getters and setters - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - public int getThreadPoolSize() { return threadPoolSize; } - public void setThreadPoolSize(int threadPoolSize) { this.threadPoolSize = threadPoolSize; } - public int getDataProviderThreadCount() { return dataProviderThreadCount; } - public void setDataProviderThreadCount(int dataProviderThreadCount) { this.dataProviderThreadCount = dataProviderThreadCount; } } + @Setter + @Getter public static class ApiConfig { + // getters and setters private int connectionTimeout = 30000; private int socketTimeout = 30000; private int maxRetries = 3; private boolean logRequestResponse = false; - - // getters and setters - public int getConnectionTimeout() { return connectionTimeout; } - public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } - public int getSocketTimeout() { return socketTimeout; } - public void setSocketTimeout(int socketTimeout) { this.socketTimeout = socketTimeout; } - public int getMaxRetries() { return maxRetries; } - public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } - public boolean isLogRequestResponse() { return logRequestResponse; } - public void setLogRequestResponse(boolean logRequestResponse) { this.logRequestResponse = logRequestResponse; } } + @Setter + @Getter public static class UiConfig { + // getters and setters private boolean headless = true; private int implicitWait = 10; private int pageLoadTimeout = 30; private String windowSize = "1920x1080"; private boolean enableVideoRecording = false; - - // getters and setters - public boolean isHeadless() { return headless; } - public void setHeadless(boolean headless) { this.headless = headless; } - public int getImplicitWait() { return implicitWait; } - public void setImplicitWait(int implicitWait) { this.implicitWait = implicitWait; } - public int getPageLoadTimeout() { return pageLoadTimeout; } - public void setPageLoadTimeout(int pageLoadTimeout) { this.pageLoadTimeout = pageLoadTimeout; } - public String getWindowSize() { return windowSize; } - public void setWindowSize(String windowSize) { this.windowSize = windowSize; } - public boolean isEnableVideoRecording() { return enableVideoRecording; } - public void setEnableVideoRecording(boolean enableVideoRecording) { this.enableVideoRecording = enableVideoRecording; } } - // Main getters and setters - public int getMaxRetries() { return maxRetries; } - public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } - public int getThreadCount() { return threadCount; } - public void setThreadCount(int threadCount) { this.threadCount = threadCount; } - public int getTimeoutSeconds() { return timeoutSeconds; } - public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } - public boolean isTakeScreenshotOnFailure() { return takeScreenshotOnFailure; } - public void setTakeScreenshotOnFailure(boolean takeScreenshotOnFailure) { this.takeScreenshotOnFailure = takeScreenshotOnFailure; } - public boolean isEnableDetailedReporting() { return enableDetailedReporting; } - public void setEnableDetailedReporting(boolean enableDetailedReporting) { this.enableDetailedReporting = enableDetailedReporting; } - public String getDefaultBrowser() { return defaultBrowser; } - public void setDefaultBrowser(String defaultBrowser) { this.defaultBrowser = defaultBrowser; } - public ParallelExecution getParallelExecution() { return parallelExecution; } - public void setParallelExecution(ParallelExecution parallelExecution) { this.parallelExecution = parallelExecution; } - public ApiConfig getApi() { return api; } - public void setApi(ApiConfig api) { this.api = api; } - public UiConfig getUi() { return ui; } - public void setUi(UiConfig ui) { this.ui = ui; } } diff --git a/common/src/main/java/com/cmccarthy/common/service/RestService.java b/common/src/main/java/com/cmccarthy/common/service/RestService.java index 150201b..f97baae 100644 --- a/common/src/main/java/com/cmccarthy/common/service/RestService.java +++ b/common/src/main/java/com/cmccarthy/common/service/RestService.java @@ -35,13 +35,11 @@ public void init() { private void setupRestAssuredConfig() { TestConfiguration.ApiConfig apiConfig = testConfiguration.getApi(); - - RestAssuredConfig config = RestAssuredConfig.config() + + RestAssured.config = RestAssuredConfig.config() .httpClient(HttpClientConfig.httpClientConfig() .setParam("http.connection.timeout", apiConfig.getConnectionTimeout()) .setParam("http.socket.timeout", apiConfig.getSocketTimeout())); - - RestAssured.config = config; if (apiConfig.isLogRequestResponse() && logManager != null) { RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); @@ -60,23 +58,13 @@ public Response executeWithRetry(RequestSpecification request, String method, St logManager.info("Executing " + method + " request to: " + endpoint); } - Response response; - switch (method.toUpperCase()) { - case "GET": - response = request.get(endpoint); - break; - case "POST": - response = request.post(endpoint); - break; - case "PUT": - response = request.put(endpoint); - break; - case "DELETE": - response = request.delete(endpoint); - break; - default: - throw new IllegalArgumentException("Unsupported HTTP method: " + method); - } + Response response = switch (method.toUpperCase()) { + case "GET" -> request.get(endpoint); + case "POST" -> request.post(endpoint); + case "PUT" -> request.put(endpoint); + case "DELETE" -> request.delete(endpoint); + default -> throw new IllegalArgumentException("Unsupported HTTP method: " + method); + }; if (logManager != null) { logManager.info("Response status: " + response.getStatusCode()); diff --git a/common/src/main/java/com/cmccarthy/common/utils/ApplicationProperties.java b/common/src/main/java/com/cmccarthy/common/utils/ApplicationProperties.java index a512073..d93bbad 100644 --- a/common/src/main/java/com/cmccarthy/common/utils/ApplicationProperties.java +++ b/common/src/main/java/com/cmccarthy/common/utils/ApplicationProperties.java @@ -1,8 +1,10 @@ package com.cmccarthy.common.utils; +import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +@Getter @Component public class ApplicationProperties { @@ -15,31 +17,16 @@ public class ApplicationProperties { @Value("${gridUrl}") private String gridUrl; - public String getWeatherAppUrl() { - return weatherAppUrl; - } - public void setWeatherAppUrl(String weatherAppUrl) { this.weatherAppUrl = weatherAppUrl; } - public String getWikipediaUrl() { - return wikipediaUrl; - } - public void setWikipediaUrl(String wikipediaUrl) { this.wikipediaUrl = wikipediaUrl; } - public String getBrowser() { - return browser; - } - public void setBrowser(String browser) { this.browser = browser; } - public String getGridUrl() { - return gridUrl; - } } \ No newline at end of file diff --git a/common/src/main/java/com/cmccarthy/common/utils/Constants.java b/common/src/main/java/com/cmccarthy/common/utils/Constants.java index 86ae31b..d0df20f 100644 --- a/common/src/main/java/com/cmccarthy/common/utils/Constants.java +++ b/common/src/main/java/com/cmccarthy/common/utils/Constants.java @@ -10,8 +10,4 @@ public class Constants { public static final long pollingShort = 100; - public static String DRIVER_DIRECTORY = System.getProperty("user.dir") + "/src/test/resources/drivers"; - - public static String COMMON_RESOURCES = System.getProperty("user.dir") + "/../common/src/main/resources"; - } diff --git a/common/src/main/java/com/cmccarthy/common/utils/DateTimeUtil.java b/common/src/main/java/com/cmccarthy/common/utils/DateTimeUtil.java index 4af8537..4fa61a7 100644 --- a/common/src/main/java/com/cmccarthy/common/utils/DateTimeUtil.java +++ b/common/src/main/java/com/cmccarthy/common/utils/DateTimeUtil.java @@ -101,22 +101,15 @@ public static String featureDateManager(String tableDate) { final LocalDateTime date = LocalDateTime.now(); - switch (switchType) { - case "Day +": - return date.plusDays(dateValue).format(ISO_DATE_FORMAT_NO_TIME); - case "Day -": - return date.minusDays(dateValue).format(ISO_DATE_FORMAT_NO_TIME); - case "Month +": - return date.plusMonths(dateValue).format(ISO_DATE_FORMAT_NO_TIME); - case "Month -": - return date.minusMonths(dateValue).format(ISO_DATE_FORMAT_NO_TIME); - case "Year +": - return date.plusYears(dateValue).format(ISO_DATE_FORMAT_NO_TIME); - case "Year -": - return date.minusYears(dateValue).format(ISO_DATE_FORMAT_NO_TIME); - default: - return date.format(ISO_DATE_FORMAT_NO_TIME); - } + return switch (switchType) { + case "Day +" -> date.plusDays(dateValue).format(ISO_DATE_FORMAT_NO_TIME); + case "Day -" -> date.minusDays(dateValue).format(ISO_DATE_FORMAT_NO_TIME); + case "Month +" -> date.plusMonths(dateValue).format(ISO_DATE_FORMAT_NO_TIME); + case "Month -" -> date.minusMonths(dateValue).format(ISO_DATE_FORMAT_NO_TIME); + case "Year +" -> date.plusYears(dateValue).format(ISO_DATE_FORMAT_NO_TIME); + case "Year -" -> date.minusYears(dateValue).format(ISO_DATE_FORMAT_NO_TIME); + default -> date.format(ISO_DATE_FORMAT_NO_TIME); + }; } return null; } diff --git a/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java b/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java index 05a6d26..09f343a 100644 --- a/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java +++ b/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java @@ -5,6 +5,7 @@ import com.cmccarthy.common.utils.ApplicationProperties; import com.cmccarthy.common.utils.LogManager; import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -21,12 +22,14 @@ public class WeatherService { private StepDefinitionDataManager stepDefinitionDataManager; @Autowired private ApplicationProperties applicationProperties; + private Response response; public void getWeatherForLocation(String location) { - Response response = restService.getRequestSpecification() - .param("q", location) - .param("appid", "0a1b11f110d4b6cd43181d23d724cb94") - .get(applicationProperties.getWeatherAppUrl()); + + final RequestSpecification requestSpecification = restService.getRequestSpecification().param("q", location) + .param("appid", "0a1b11f110d4b6cd43181d23d724cb94"); + + Response response = restService.executeWithRetry(requestSpecification, "GET", applicationProperties.getWeatherAppUrl()); stepDefinitionDataManager.addToStoredObjectMap("class", response); diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index 16fb029..2852eab 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -72,7 +72,7 @@ public void createDriver() throws IOException { } } - public void setLocalWebDriver() throws IOException { + public void setLocalWebDriver() { String browser = applicationProperties.getBrowser(); boolean isHeadless = testConfig.getUi().isHeadless(); @@ -183,10 +183,6 @@ public WebDriver getDriver() { return driverThreadLocal.get(); } - public void setDriver(WebDriver driver) { - driverThreadLocal.set(driver); - } - public JavascriptExecutor getJSExecutor() { return (JavascriptExecutor) getDriver(); } From 02ebb1d8c7915267b2afe394cbbbb1513d0c7de0 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 16:57:36 +1000 Subject: [PATCH 18/28] init commit --- README.md | 421 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 348 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index fc56f02..6a8851f 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,383 @@ -# Spring Cucumber TestNG Parallel Test Harness +# Cucumber Automated Testing Framework -A robust test automation framework built using Spring Boot, Cucumber, and TestNG for parallel test execution. This project demonstrates how to organize and execute automated tests efficiently in parallel across multiple modules. +[![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) -## 🚀 Features +# Index + + + + + + + + + + + + + + + + + +
Start + | Maven + | Quickstart | +
Run + | TestNG + | Command Line + | IDE Support + | Java JDK + | Troubleshooting | +
Report + | Configuration + | Environment Switching + | Spark HTML Reports + | Logging | +
Advanced + | Before / After Hooks + | JSON Transforms + | Contributing | +
-- **Multi-Module Architecture**: Separate modules for common functionality, weather tests, and Wikipedia tests -- **Parallel Execution**: Run tests concurrently using TestNG -- **Spring Boot Integration**: Leverage dependency injection and application properties -- **Comprehensive Reporting**: Allure and Extent reports for detailed test results -- **Logging**: Dedicated logs for each test scenario -- **Cross-Environment Support**: Multiple environment configurations (dev, uat, prod) +# Maven -## 📋 Requirements +The Framework uses [Spring Boot Test](https://spring.io/guides/gs/testing-web/), [Cucumber](https://cucumber.io/) +, [Rest Assured](https://rest-assured.io/) and [Selenium](https://www.selenium.dev/) client implementations. -- Java 11 or higher -- Maven 3.6 or higher -- Git +Spring ``: -## 🔧 Setup & Installation +```xml -1. Clone the repository: - ```bash - git clone https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness.git - cd spring-cucumber-testng-parallel-test-harness - ``` + + ... + + org.springframework.amqp + spring-rabbit + ${spring-rabbit.version} + + + org.springframework.boot + spring-boot-starter-test + + + org.springframework + spring-test + + ... + +``` -2. Install dependencies: - ```bash - mvn clean install -DskipTests - ``` +Cucumber & Rest Assured ``: -## 🏃‍♂️ Running Tests +```xml -### Running All Tests -```bash -mvn clean test + + ... + + io.rest-assured + rest-assured + ${restassured.version} + + + io.cucumber + cucumber-java + ${cucumber.version} + + + io.cucumber + cucumber-spring + ${cucumber.version} + + + io.cucumber + cucumber-testng + ${cucumber.version} + + ... + ``` -### Running Tests for a Specific Module -```bash -mvn clean test -pl weather -# or -mvn clean test -pl wikipedia +Selenium ``: + +```xml + + + ... + + org.seleniumhq.selenium + selenium-java + ${selenium-version} + + + org.seleniumhq.selenium + selenium-server + ${selenium-version} + + ... + ``` -### Running with Specific Environment -```bash -mvn clean test -Dspring.profiles.active=dev +# Quickstart + +- [Intellij IDE](https://www.jetbrains.com/idea/) - `Recommended` +- [Java JDK 17](https://jdk.java.net/java-se-ri/11) +- [Apache Maven](https://maven.apache.org/docs/3.6.3/release-notes.html) + +# TestNG + +By using the [TestNG Framework](https://junit.org/junit4/) we can utilize the [Cucumber Framework](https://cucumber.io/) +and the `@CucumberOptions` Annotation Type to execute the `*.feature` file tests + +> Right click the `WikipediParallelRunner` class and select `Run` + +```java + +@CucumberOptions( + features = { + "src/test/resources/feature" + }, + plugin = { + "pretty", + "json:target/cucumber/report.json", + "com.aventstack.extentreports.cucumber.adapter.ExtentCucumberAdapter:" + }) +public class WikipediaParallelRunnerTest extends AbstractTestNGCucumberTests { + + @Override + @DataProvider(parallel = true) + public Object[][] scenarios() { + return super.scenarios(); + } + +} ``` -Available profiles: `dev`, `uat`, `prod`, `headless-github`, `cloud-provider` +# Command Line -## 📊 Reporting +Normally you will use your IDE to run a `*.feature` file directly or via the `*Test.java` class. With the `Test` class, +we can run tests from the command-line as well. -After test execution, reports are available at: +Note that the `mvn test` command only runs test classes that follow the `*Test.java` naming convention. -- **Allure Reports**: `/target/allure-results/` -- **Extent Reports**: `/target/cucumber/report.html` -- **TestNG Reports**: `/target/surefire-reports/index.html` +You can run a single test or a suite or tests like so : -To generate and open Allure reports: -```bash -mvn allure:serve +``` +mvn test -Dtest=WikipediaParallelRunnerTest ``` -## 📁 Project Structure +Note that the `mvn clean install` command runs all test Classes that follow the `*Test.java` naming convention -- **common**: Shared utilities, base classes, and configurations - - Configuration properties - - Common test utilities - - Shared step definitions - -- **weather**: Weather API testing module - - Feature files for weather API tests - - Step definitions for weather tests - - API client for weather services +``` +mvn clean install +``` -- **wikipedia**: Wikipedia functionality tests - - Feature files for Wikipedia tests - - Step definitions for Wikipedia interactions - - Page objects for Wikipedia pages +# IDE Support -## 🌐 Environment Configuration +To minimize the discrepancies between IDE versions and Locales the `` is set to `UTF-8` -The framework supports multiple environments through Spring profiles: +```xml -- **dev**: Development environment -- **uat**: User Acceptance Testing environment -- **prod**: Production environment -- **headless-github**: Headless browser configuration for CI/CD -- **cloud-provider**: Configuration for cloud-based test execution + + ... + UTF-8 + UTF-8 + ... + +``` -## 📝 Logging +# Java JDK + +The Java version to use is defined in the `maven-compiler-plugin` + +```xml + + + ... + + + ... + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + ... + + + ... + +``` + +# Configuration + +The `AbstractTestDefinition` class is responsible for specifying each Step class as `@SpringBootTest` and +its `@ContextConfiguration` + +```java + +@ContextConfiguration(classes = {FrameworkContextConfiguration.class}) +@SpringBootTest +public class AbstractTestDefinition { +} +``` + +The `FrameworkContextConfiguration` class is responsible for specifying the Spring `@Configuration`, modules to scan, +properties to use etc + +```java + +@EnableRetry +@Configuration +@ComponentScan({ + "com.cmccarthy.api", "com.cmccarthy.common", +}) +@PropertySource("application.properties") +public class FrameworkContextConfiguration { +} +``` + +# Environment Switching + +There is only one thing you need to do to switch the environment - which is to set `` property in the +Master POM. + +> By default, the value of `spring.profiles.active` is defined in the `application.properties` file which inherits its +> value from the Master POM property `` + +```xml + + + ... + + prod + + true + + + prod + + + ... + +``` + +You can then specify the profile to use when running Maven from the command line like so: + +``` +mvn clean install -DactiveProfile=github-headless +``` + +Below is an example of the `application.properties` file. + +```properties +spring.profiles.active=@activatedProperties@ +``` + +# Extent Reports + +The Framework uses [Extent Reports Framework](https://extentreports.com/) to generate the HTML Test Reports + +The example below is a report generated automatically by Extent Reports open-source library. + + + +# Allure Reports + +The Framework uses [Allure Reports](https://docs.qameta.io/allure/) to generate the HTML Test Reports + +The example below is a report generated by Allure Reports open-source library. + + + +To generate the above report navigate to the root directory of the module under test and execute the following command + +`mvn allure:serve` or `mvn allure:generate` (for an offline report) + +# Logging + +The Framework uses [Log4j2](https://logging.apache.org/log4j/2.x/) You can instantiate the logging service in any Class +like so + +```java +private final Logger logger=LoggerFactory.getLogger(WikipediaPageSteps.class); +``` + +you can then use the logger like so : + +```java +logger.info("This is a info message"); + logger.warn("This is a warning message"); + logger.debug("This is a info message"); + logger.error("This is a error message"); +``` + +# Before / After Hooks + +The [Logback](http://logback.qos.ch/) logging service is initialized from the `Hooks.class` + +As the Cucumber Hooks are implemented by all steps we can configure the `@CucumberContextConfiguration` like so : + +```java + +@CucumberContextConfiguration +public class Hooks extends AbstractTestDefinition { + + private static boolean initialized = false; + private static final Object lock = new Object(); + + @Autowired + private HookUtil hookUtil; + @Autowired + private DriverManager driverManager; + + @Before + public void beforeScenario(Scenario scenario) { + synchronized (lock) { + if (!initialized) { + if (!driverManager.isDriverExisting()) { + driverManager.downloadDriver(); + } + initialized = true; + } + } + driverManager.createDriver(); + } + + @After + public void afterScenario(Scenario scenario) { + hookUtil.endOfTest(scenario); + WebDriverRunner.closeWebDriver(); + } +} +``` + +# JSON Transforms + +[Rest Assured IO](https://rest-assured.io/) is used to map the `Response` Objects to their respective `POJO` Classes + +```xml + + + io.rest-assured + rest-assured + 3.0.0 + +``` -Test execution logs are stored in the `logs` directory of each module. Log files are named after the test scenario for easy debugging. +# Troubleshooting -## 🤝 Contributing +- Execute the following commands to resolve any dependency issues + 1. `cd ~/install directory path/spring-cucumber-testng-parallel-test-harness` + 2. `mvn clean install -DskipTests` -Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. +# Contributing -## 📄 License +Spotted a mistake? Questions? Suggestions? -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +[Open an Issue](https://github.com/cmccarthyIrl/spring-cucumber-testng-parallel-test-harness/issues) -## 👥 Maintainers -- [cmccarthyIrl](https://github.com/cmccarthyIrl) From ec4b777a4f62a05e3e1c8fe300bdce57d5d8a5c3 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:03:40 +1000 Subject: [PATCH 19/28] init commit --- .../cmccarthy/common/utils/StringUtil.java | 40 +++++++++++++++---- .../java/com/cmccarthy/ui/step/Hooks.java | 3 -- .../com/cmccarthy/ui/utils/DriverWait.java | 1 + 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java b/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java index acc2174..7c987f8 100644 --- a/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java +++ b/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java @@ -1,15 +1,33 @@ package com.cmccarthy.common.utils; +import java.security.SecureRandom; import java.util.NoSuchElementException; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; -import static org.apache.commons.lang3.RandomStringUtils.random; - @SuppressWarnings("unused") public class StringUtil { private static final Random random = new Random(); + private static final SecureRandom secureRandom = new SecureRandom(); + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final String NUMBERS = "0123456789"; + private static final String ALPHANUMERIC = ALPHABET + NUMBERS; + + /** + * Generates a random string from the given character set. + * + * @param length the length of the string to generate + * @param charset the character set to use + * @return a random string + */ + private static String generateRandomString(int length, String charset) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(charset.charAt(secureRandom.nextInt(charset.length()))); + } + return sb.toString(); + } public static boolean getRandomBoolean() { return random.nextBoolean(); @@ -24,27 +42,33 @@ public static int getRandomNumber(int min, int max) { } public static String getRandomAlphaString(int length) { - return random(length, true, false); + return generateRandomString(length, ALPHABET); } public static String getRandomAlphaString(int min, int max) { - return random(getRandomNumber(min, max), true, false); + return generateRandomString(getRandomNumber(min, max), ALPHABET); } public static String getRandomNumericString(int length, int min, int max) { - return random(length, min, max, false, true); + // Generate numeric string with values between min and max + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int digit = ThreadLocalRandom.current().nextInt(min, max + 1) % 10; + sb.append(digit); + } + return sb.toString(); } public static String getRandomNumericString(int min, int max) { - return random(getRandomNumber(min, max), false, true); + return generateRandomString(getRandomNumber(min, max), NUMBERS); } public static String getRandomAlphaNumericString(int length) { - return random(length, true, true); + return generateRandomString(length, ALPHANUMERIC); } public static String getRandomAlphaNumericString(int min, int max) { - return random(getRandomNumber(min, max), true, true); + return generateRandomString(getRandomNumber(min, max), ALPHANUMERIC); } public static String getRandomAmount(String min, String max) { diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java index 4326d6a..2a4d984 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java @@ -15,9 +15,6 @@ @CucumberContextConfiguration public class Hooks extends WikipediaAbstractTestDefinition { - private static boolean initialized = false; - private static final Object lock = new Object(); - @Autowired private LogManager logManager; @Autowired diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverWait.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverWait.java index e0c3eaf..3e2b3e9 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverWait.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverWait.java @@ -17,6 +17,7 @@ import java.util.NoSuchElementException; @Component +@SuppressWarnings("unused") public class DriverWait { private static final ThreadLocal> driverWaitThreadLocal = new ThreadLocal<>(); From 4b38f4576947752a5453b54456c63726620bb414 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:04:17 +1000 Subject: [PATCH 20/28] init commit --- .../src/test/java/com/cmccarthy/api/service/WeatherService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java b/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java index 09f343a..3f821d4 100644 --- a/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java +++ b/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java @@ -22,7 +22,6 @@ public class WeatherService { private StepDefinitionDataManager stepDefinitionDataManager; @Autowired private ApplicationProperties applicationProperties; - private Response response; public void getWeatherForLocation(String location) { From 944bbf82181b94e35ab4903ba52190e77d79b9b0 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:13:43 +1000 Subject: [PATCH 21/28] init commit --- .github/workflows/enhanced-ci.yml | 105 ------------------------------ .github/workflows/run.yml | 98 +++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 123 deletions(-) delete mode 100644 .github/workflows/enhanced-ci.yml diff --git a/.github/workflows/enhanced-ci.yml b/.github/workflows/enhanced-ci.yml deleted file mode 100644 index 2cae04e..0000000 --- a/.github/workflows/enhanced-ci.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Enhanced CI/CD Pipeline - -on: - pull_request: - branches: [ "main", "develop" ] - -env: - JAVA_VERSION: '17' - MAVEN_OPTS: '-Xmx2048m' - -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - strategy: - matrix: - module: [weather, wikipedia] - browser: [chrome, firefox] - fail-fast: false - max-parallel: 4 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: 'temurin' - cache: maven - - - name: Cache Maven dependencies - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Setup Chrome - if: matrix.browser == 'chrome' - uses: browser-actions/setup-chrome@v1 - with: - chrome-version: stable - - - name: Setup Firefox - if: matrix.browser == 'firefox' - uses: browser-actions/setup-firefox@v1 - with: - firefox-version: latest - - - name: Compile project - run: mvn clean compile test-compile - - - name: Run ${{ matrix.module }} tests with ${{ matrix.browser }} - run: | - cd ${{ matrix.module }} - mvn test -Dbrowser=${{ matrix.browser }} -Dtest.ui.headless=true -Dspring.profiles.active=headless-github - env: - BROWSER: ${{ matrix.browser }} - HEADLESS: true - - - name: Generate Allure Report - if: always() - run: | - cd ${{ matrix.module }} - mvn allure:report - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-${{ matrix.module }}-${{ matrix.browser }} - path: | - ${{ matrix.module }}/target/allure-results/ - ${{ matrix.module }}/target/cucumber/ - ${{ matrix.module }}/logs/ - retention-days: 30 - - - name: Upload screenshots on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: screenshots-${{ matrix.module }}-${{ matrix.browser }} - path: ${{ matrix.module }}/screenshots/ - retention-days: 7 - - publish-reports: - name: Publish Test Reports - needs: test - runs-on: ubuntu-latest - if: always() - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - - - name: Publish Test Report - uses: dorny/test-reporter@v1 - if: always() - with: - name: Test Results - path: '**/target/cucumber/*.json' - reporter: java-junit - fail-on-error: false diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 5738ce2..45dd6ea 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -6,32 +6,94 @@ on: push: branches: [ "feature/wip" ] +# Concurrency control to cancel previous runs on new commits +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + strategy: + matrix: + java: [17] + os: [ubuntu-latest, ubuntu-22.04] + fail-fast: false - runs-on: ubuntu-latest + env: + DISPLAY: ':99' + MAVEN_OPTS: '-Xmx2048m' steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 + - uses: actions/checkout@v4.1.7 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v4.3.0 with: - java-version: '17' + java-version: '${{ matrix.java }}' distribution: 'temurin' cache: maven - - name: Remove Chrome - run: sudo apt purge google-chrome-stable - - name: Remove default Chromium - run: sudo apt purge chromium-browser - - - name: Install Google Chrome # Using shell script to install Google Chrome + + - name: Cache Maven dependencies + uses: actions/cache@v4.0.2 + with: + path: | + ~/.m2/repository + ~/.m2/wrapper + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Set up Chrome + uses: browser-actions/setup-chrome@v1.7.2 + with: + chrome-version: stable + + - name: Start Xvfb run: | - chmod +x ./.github/scripts/InstallChrome.sh - ./.github/scripts/InstallChrome.sh - - - run: | - export DISPLAY=:99 - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional + sudo Xvfb :99 -ac -screen 0 1280x1024x24 > /dev/null 2>&1 & + sleep 3 + + - name: Validate environment + run: | + echo "Java version:" + java -version + echo "Maven version:" + mvn -version + echo "Chrome version:" + google-chrome --version + echo "Display: $DISPLAY" - name: Test - run: mvn clean install -DactiveProfile=headless-github \ No newline at end of file + run: mvn clean install -P headless-github -B -T 1C + continue-on-error: false + timeout-minutes: 20 + + - name: Upload test reports + uses: actions/upload-artifact@v4.4.0 + if: always() + with: + name: test-reports-java${{ matrix.java }}-${{ matrix.os }} + path: | + **/target/surefire-reports/ + **/target/allure-results/ + **/target/cucumber/ + **/logs/ + retention-days: 30 + + - name: Upload screenshots on failure + uses: actions/upload-artifact@v4.4.0 + if: failure() + with: + name: screenshots-java${{ matrix.java }}-${{ matrix.os }} + path: | + **/target/screenshots/ + **/target/test-output/ + retention-days: 7 + + - name: Cleanup + if: always() + run: | + pkill -f Xvfb || true + pkill -f chrome || true \ No newline at end of file From 13f51c8144ac57d0e25a7454383994a30b5b76bb Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:15:21 +1000 Subject: [PATCH 22/28] init commit --- .github/workflows/run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 45dd6ea..567ebef 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -37,7 +37,7 @@ jobs: cache: maven - name: Cache Maven dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.2.3 with: path: | ~/.m2/repository From 20995aa68392da7a6c7a98f98b6f05ac9499f8a9 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:18:04 +1000 Subject: [PATCH 23/28] init commit --- .github/workflows/run.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 567ebef..dbc1136 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -20,11 +20,13 @@ jobs: matrix: java: [17] os: [ubuntu-latest, ubuntu-22.04] + browser: [chrome, firefox] fail-fast: false env: DISPLAY: ':99' MAVEN_OPTS: '-Xmx2048m' + BROWSER: ${{ matrix.browser }} steps: - uses: actions/checkout@v4.1.7 @@ -46,10 +48,17 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Set up Chrome + if: matrix.browser == 'chrome' uses: browser-actions/setup-chrome@v1.7.2 with: chrome-version: stable + - name: Set up Firefox + if: matrix.browser == 'firefox' + uses: browser-actions/setup-firefox@v1.5.2 + with: + firefox-version: latest + - name: Start Xvfb run: | sudo Xvfb :99 -ac -screen 0 1280x1024x24 > /dev/null 2>&1 & @@ -61,12 +70,18 @@ jobs: java -version echo "Maven version:" mvn -version - echo "Chrome version:" - google-chrome --version + echo "Browser: ${{ matrix.browser }}" + if [ "${{ matrix.browser }}" = "chrome" ]; then + echo "Chrome version:" + google-chrome --version + elif [ "${{ matrix.browser }}" = "firefox" ]; then + echo "Firefox version:" + firefox --version + fi echo "Display: $DISPLAY" - name: Test - run: mvn clean install -P headless-github -B -T 1C + run: mvn clean install -P headless-github -B -T 1C -Dbrowser=${{ matrix.browser }} continue-on-error: false timeout-minutes: 20 @@ -74,7 +89,7 @@ jobs: uses: actions/upload-artifact@v4.4.0 if: always() with: - name: test-reports-java${{ matrix.java }}-${{ matrix.os }} + name: test-reports-java${{ matrix.java }}-${{ matrix.os }}-${{ matrix.browser }} path: | **/target/surefire-reports/ **/target/allure-results/ @@ -86,7 +101,7 @@ jobs: uses: actions/upload-artifact@v4.4.0 if: failure() with: - name: screenshots-java${{ matrix.java }}-${{ matrix.os }} + name: screenshots-java${{ matrix.java }}-${{ matrix.os }}-${{ matrix.browser }} path: | **/target/screenshots/ **/target/test-output/ @@ -96,4 +111,5 @@ jobs: if: always() run: | pkill -f Xvfb || true - pkill -f chrome || true \ No newline at end of file + pkill -f chrome || true + pkill -f firefox || true \ No newline at end of file From faba95cdd08e62a45421a85c13a59b8f42cb0c34 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:20:00 +1000 Subject: [PATCH 24/28] init commit --- .github/workflows/run.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index dbc1136..5ee2eb1 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -55,9 +55,15 @@ jobs: - name: Set up Firefox if: matrix.browser == 'firefox' - uses: browser-actions/setup-firefox@v1.5.2 - with: - firefox-version: latest + run: | + # Install Firefox from Mozilla PPA for latest version + sudo apt-get update + sudo apt-get install -y software-properties-common + sudo add-apt-repository -y ppa:mozillateam/ppa + sudo apt-get update + sudo apt-get install -y firefox + # Verify installation + firefox --version - name: Start Xvfb run: | From 5622b2c3bcae8593d0fd8e8bdcd4a7702a988caf Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:22:12 +1000 Subject: [PATCH 25/28] init commit --- .github/workflows/run.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 5ee2eb1..4915895 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -56,10 +56,7 @@ jobs: - name: Set up Firefox if: matrix.browser == 'firefox' run: | - # Install Firefox from Mozilla PPA for latest version - sudo apt-get update - sudo apt-get install -y software-properties-common - sudo add-apt-repository -y ppa:mozillateam/ppa + # Install Firefox from Ubuntu default repository (faster and more reliable) sudo apt-get update sudo apt-get install -y firefox # Verify installation From 2f7cbbeffcbefd594db226a47ea2d70a3a77890e Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:25:48 +1000 Subject: [PATCH 26/28] init commit --- .../src/test/java/com/cmccarthy/ui/utils/DriverManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index 2852eab..b85b264 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -90,7 +90,6 @@ public void setLocalWebDriver() { } case ("firefox") -> { FirefoxOptions firefoxOptions = new FirefoxOptions(); - firefoxOptions.setCapability("marionette", true); if (isHeadless) { firefoxOptions.addArguments("--headless"); } From a6601d3f6aca309ea865e3405831cca1665aea44 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:29:32 +1000 Subject: [PATCH 27/28] init commit --- .github/workflows/run.yml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 4915895..5814b62 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -20,7 +20,7 @@ jobs: matrix: java: [17] os: [ubuntu-latest, ubuntu-22.04] - browser: [chrome, firefox] + browser: [chrome] fail-fast: false env: @@ -53,15 +53,6 @@ jobs: with: chrome-version: stable - - name: Set up Firefox - if: matrix.browser == 'firefox' - run: | - # Install Firefox from Ubuntu default repository (faster and more reliable) - sudo apt-get update - sudo apt-get install -y firefox - # Verify installation - firefox --version - - name: Start Xvfb run: | sudo Xvfb :99 -ac -screen 0 1280x1024x24 > /dev/null 2>&1 & @@ -77,9 +68,6 @@ jobs: if [ "${{ matrix.browser }}" = "chrome" ]; then echo "Chrome version:" google-chrome --version - elif [ "${{ matrix.browser }}" = "firefox" ]; then - echo "Firefox version:" - firefox --version fi echo "Display: $DISPLAY" @@ -114,5 +102,4 @@ jobs: if: always() run: | pkill -f Xvfb || true - pkill -f chrome || true - pkill -f firefox || true \ No newline at end of file + pkill -f chrome || true \ No newline at end of file From a46452a6dcc3a25cb5d205e53798c7a1aa2c8435 Mon Sep 17 00:00:00 2001 From: Craig Mc Carthy Date: Sun, 13 Jul 2025 17:31:40 +1000 Subject: [PATCH 28/28] refactored code base --- .../common/config/TestConfiguration.java | 6 ++--- .../cmccarthy/common/service/RestService.java | 8 +++--- .../cmccarthy/common/utils/StringUtil.java | 2 +- .../config/WikipediaContextConfiguration.java | 4 +-- .../java/com/cmccarthy/ui/step/Hooks.java | 2 +- .../com/cmccarthy/ui/utils/DriverHelper.java | 10 ++++---- .../com/cmccarthy/ui/utils/DriverManager.java | 25 +++++++++---------- 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java index 6558699..1c8cd14 100644 --- a/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java +++ b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java @@ -18,13 +18,13 @@ public class TestConfiguration { private boolean takeScreenshotOnFailure = true; private boolean enableDetailedReporting = true; private String defaultBrowser = "chrome"; - + // Parallel execution settings private ParallelExecution parallelExecution = new ParallelExecution(); - + // API testing settings private ApiConfig api = new ApiConfig(); - + // UI testing settings private UiConfig ui = new UiConfig(); diff --git a/common/src/main/java/com/cmccarthy/common/service/RestService.java b/common/src/main/java/com/cmccarthy/common/service/RestService.java index f97baae..5d13ece 100644 --- a/common/src/main/java/com/cmccarthy/common/service/RestService.java +++ b/common/src/main/java/com/cmccarthy/common/service/RestService.java @@ -9,11 +9,11 @@ import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; +import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; -import jakarta.annotation.PostConstruct; import static io.restassured.RestAssured.given; @@ -40,7 +40,7 @@ private void setupRestAssuredConfig() { .httpClient(HttpClientConfig.httpClientConfig() .setParam("http.connection.timeout", apiConfig.getConnectionTimeout()) .setParam("http.socket.timeout", apiConfig.getSocketTimeout())); - + if (apiConfig.isLogRequestResponse() && logManager != null) { RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); } @@ -57,7 +57,7 @@ public Response executeWithRetry(RequestSpecification request, String method, St if (logManager != null) { logManager.info("Executing " + method + " request to: " + endpoint); } - + Response response = switch (method.toUpperCase()) { case "GET" -> request.get(endpoint); case "POST" -> request.post(endpoint); @@ -70,7 +70,7 @@ public Response executeWithRetry(RequestSpecification request, String method, St logManager.info("Response status: " + response.getStatusCode()); logManager.debug("Response body: " + response.getBody().asString()); } - + return response; } diff --git a/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java b/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java index 7c987f8..1ee3505 100644 --- a/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java +++ b/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java @@ -17,7 +17,7 @@ public class StringUtil { /** * Generates a random string from the given character set. * - * @param length the length of the string to generate + * @param length the length of the string to generate * @param charset the character set to use * @return a random string */ diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java b/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java index 58e03ed..7e85ed7 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/config/WikipediaContextConfiguration.java @@ -8,8 +8,8 @@ @EnableRetry @Configuration @ComponentScan({ - "com.cmccarthy.ui", - "com.cmccarthy.common" + "com.cmccarthy.ui", + "com.cmccarthy.common" }) @PropertySource("classpath:/application.properties") public class WikipediaContextConfiguration { diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java index 2a4d984..c7ba190 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/step/Hooks.java @@ -14,7 +14,7 @@ @CucumberContextConfiguration public class Hooks extends WikipediaAbstractTestDefinition { - + @Autowired private LogManager logManager; @Autowired diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverHelper.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverHelper.java index 7845f49..d140546 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverHelper.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverHelper.java @@ -39,7 +39,7 @@ public void sendKeys(WebElement element, String value) { /** * Clicks on an element by WebElement */ - @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) public void click(WebElement element) throws NoSuchFieldException { try { driverWait.waitForElementToLoad(element); @@ -53,7 +53,7 @@ public void click(WebElement element) throws NoSuchFieldException { /** * Clicks on an element by Locator */ - @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) public void click(By locator) throws NoSuchFieldException { try { driverWait.waitForElementToLoad(locator); @@ -67,7 +67,7 @@ public void click(By locator) throws NoSuchFieldException { /** * Clicks on an element by Locator */ - @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = { RetryException.class }) + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) public void rightClick(By locator) throws NoSuchFieldException { driverWait.waitForElementToLoad(locator); final WebElement element = driverManager.getDriver().findElement(locator); @@ -81,7 +81,7 @@ public void rightClick(By locator) throws NoSuchFieldException { } } - @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) public void scrollElementIntoView(WebElement element) { try { driverManager.getJSExecutor().executeScript("arguments[0].scrollIntoView(true);", element); @@ -128,7 +128,7 @@ public void clickAction(WebElement element) throws NoSuchFieldException { /** * Clicks on an element using Actions */ - @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500), retryFor = {RetryException.class}) public void clickAction(By locator) throws NoSuchFieldException { driverWait.waitForElementToLoad(locator); diff --git a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java index b85b264..07e903e 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -54,7 +54,7 @@ public void createDriver() throws IOException { setLocalWebDriver(); } getDriver().manage().deleteAllCookies();//useful for AJAX pages - + // Set the window size if specified in the configuration if (testConfig.getUi().getWindowSize() != null && !testConfig.getUi().getWindowSize().isEmpty()) { String[] dimensions = testConfig.getUi().getWindowSize().split("x"); @@ -75,7 +75,7 @@ public void createDriver() throws IOException { public void setLocalWebDriver() { String browser = applicationProperties.getBrowser(); boolean isHeadless = testConfig.getUi().isHeadless(); - + switch (browser) { case ("chrome") -> { ChromeOptions options = new ChromeOptions(); @@ -111,18 +111,17 @@ public void setLocalWebDriver() { } driverThreadLocal.set(new EdgeDriver(edgeOptions)); } - default -> - throw new NoSuchElementException("Failed to create an instance of WebDriver for: " + browser); + default -> throw new NoSuchElementException("Failed to create an instance of WebDriver for: " + browser); } - + // Configure WebDriver timeouts WebDriver driver = driverThreadLocal.get(); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(testConfig.getUi().getImplicitWait())); driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(testConfig.getUi().getPageLoadTimeout())); - + driverWait.getDriverWaitThreadLocal() - .set(new WebDriverWait(driver, - Duration.ofSeconds(testConfig.getTimeoutSeconds()), + .set(new WebDriverWait(driver, + Duration.ofSeconds(testConfig.getTimeoutSeconds()), Duration.ofMillis(Constants.pollingShort))); } @@ -130,7 +129,7 @@ private void setRemoteDriver(URL hubUrl) { Capabilities capability; String browser = applicationProperties.getBrowser(); boolean isHeadless = testConfig.getUi().isHeadless(); - + switch (browser) { case "firefox" -> { FirefoxOptions options = new FirefoxOptions(); @@ -166,15 +165,15 @@ private void setRemoteDriver(URL hubUrl) { default -> throw new NoSuchElementException("Failed to create an instance of RemoteWebDriver for: " + browser); } - + // Configure WebDriver timeouts WebDriver driver = driverThreadLocal.get(); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(testConfig.getUi().getImplicitWait())); driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(testConfig.getUi().getPageLoadTimeout())); - + driverWait.getDriverWaitThreadLocal() - .set(new WebDriverWait(driver, - Duration.ofSeconds(testConfig.getTimeoutSeconds()), + .set(new WebDriverWait(driver, + Duration.ofSeconds(testConfig.getTimeoutSeconds()), Duration.ofMillis(Constants.pollingShort))); }