diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index d55216e..5814b62 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -4,35 +4,102 @@ on: pull_request: branches: [ "master" ] push: - branches: [ "feature/WebDriverManager" ] + 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] + browser: [chrome] + fail-fast: false - runs-on: ubuntu-latest + env: + DISPLAY: ':99' + MAVEN_OPTS: '-Xmx2048m' + BROWSER: ${{ matrix.browser }} 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.2.3 + with: + path: | + ~/.m2/repository + ~/.m2/wrapper + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + 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: Start Xvfb 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 + 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 "Browser: ${{ matrix.browser }}" + if [ "${{ matrix.browser }}" = "chrome" ]; then + echo "Chrome version:" + google-chrome --version + fi + 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 -Dbrowser=${{ matrix.browser }} + 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 }}-${{ matrix.browser }} + 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 }}-${{ matrix.browser }} + 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 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..1c8cd14 --- /dev/null +++ b/common/src/main/java/com/cmccarthy/common/config/TestConfiguration.java @@ -0,0 +1,61 @@ +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; + 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(); + + @Setter + @Getter + public static class ParallelExecution { + // getters and setters + private boolean enabled = true; + private int threadPoolSize = 4; + private int dataProviderThreadCount = 4; + } + + @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; + } + + @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; + } + +} 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..5d13ece 100644 --- a/common/src/main/java/com/cmccarthy/common/service/RestService.java +++ b/common/src/main/java/com/cmccarthy/common/service/RestService.java @@ -1,6 +1,18 @@ 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 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 static io.restassured.RestAssured.given; @@ -8,7 +20,73 @@ @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(); + + RestAssured.config = RestAssuredConfig.config() + .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()); + } + } + 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" -> 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()); + 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/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/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java b/common/src/main/java/com/cmccarthy/common/utils/StringUtil.java index acc2174..1ee3505 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/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/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 diff --git a/pom.xml b/pom.xml index 8dd8df1..0eff98f 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,19 +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.0 3.1.2 3.2.3 + 2.19.0 + 3.0.0 + 4.3.0 @@ -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/java/com/cmccarthy/api/service/WeatherService.java b/weather/src/test/java/com/cmccarthy/api/service/WeatherService.java index 05a6d26..3f821d4 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; @@ -23,10 +24,11 @@ public class WeatherService { private ApplicationProperties applicationProperties; 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/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..7e85ed7 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..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,6 +14,7 @@ @CucumberContextConfiguration public class Hooks extends WikipediaAbstractTestDefinition { + @Autowired private LogManager logManager; @Autowired @@ -31,9 +32,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/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 30bdb0b..07e903e 100644 --- a/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java +++ b/wikipedia/src/test/java/com/cmccarthy/ui/utils/DriverManager.java @@ -1,8 +1,10 @@ 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; +import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; @@ -38,6 +40,8 @@ public class DriverManager { @Autowired private ApplicationProperties applicationProperties; @Autowired + private TestConfiguration testConfig; + @Autowired private DriverWait driverWait; @Autowired private Environment environment; @@ -50,23 +54,45 @@ 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"); + 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 { - switch (applicationProperties.getBrowser()) { + public void setLocalWebDriver() { + String browser = applicationProperties.getBrowser(); + 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"); - options.addArguments("--headless=new"); + if (isHeadless) { + options.addArguments("--headless=new"); + } driverThreadLocal.set(new ChromeDriver(options)); } case ("firefox") -> { FirefoxOptions firefoxOptions = new FirefoxOptions(); - firefoxOptions.setCapability("marionette", true); + if (isHeadless) { + firefoxOptions.addArguments("--headless"); + } driverThreadLocal.set(new FirefoxDriver(firefoxOptions)); } case ("ie") -> { @@ -80,27 +106,45 @@ public void setLocalWebDriver() throws IOException { } 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: " + applicationProperties.getBrowser()); + 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(driverThreadLocal.get(), Duration.ofSeconds(Constants.timeoutShort), Duration.ofSeconds(Constants.pollingShort))); + .set(new WebDriverWait(driver, + Duration.ofSeconds(testConfig.getTimeoutSeconds()), + Duration.ofMillis(Constants.pollingShort))); } private void setRemoteDriver(URL hubUrl) { Capabilities capability; - switch (applicationProperties.getBrowser()) { + String browser = applicationProperties.getBrowser(); + boolean isHeadless = testConfig.getUi().isHeadless(); + + switch (browser) { case "firefox" -> { - capability = new FirefoxOptions(); - 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 = new ChromeOptions(); options.addArguments("--no-sandbox"); options.addArguments("--disable-dev-shm-usage"); - options.addArguments("--headless"); + if (isHeadless) { + options.addArguments("--headless"); + } driverThreadLocal.set(new RemoteWebDriver(hubUrl, options)); } case "ie" -> { @@ -112,25 +156,51 @@ private void setRemoteDriver(URL hubUrl) { driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); } case ("edge") -> { - capability = new EdgeOptions(); - driverThreadLocal.set(new RemoteWebDriver(hubUrl, capability)); + 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: " + applicationProperties.getBrowser()); + 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(driverThreadLocal.get(), Duration.ofSeconds(Constants.timeoutShort), Duration.ofSeconds(Constants.pollingShort))); + .set(new WebDriverWait(driver, + Duration.ofSeconds(testConfig.getTimeoutSeconds()), + Duration.ofMillis(Constants.pollingShort))); } public WebDriver getDriver() { return driverThreadLocal.get(); } - public void setDriver(WebDriver driver) { - driverThreadLocal.set(driver); - } - public JavascriptExecutor getJSExecutor() { return (JavascriptExecutor) getDriver(); } -} + + /** + * 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 { + // Remove ThreadLocal references + driverThreadLocal.remove(); + driverWait.getDriverWaitThreadLocal().remove(); + } + } + } +} \ No newline at end of file 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<>(); 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 @@ - + - +