Skip to content

Commit

Permalink
1543 bug invalidselectorexception is thrown in some cases on some bro…
Browse files Browse the repository at this point in the history
…wseros combinations (#1545)

* optimize built-in locators

- optimize built-in locators for thread safety
- optimize JavaScriptWaitManager for thread safety and reduce webdriver calls
- remove lazyLoadingTimeout parameter and replace with the same elementIdentificationTimeout

* optimize built-in locators

- optimize built-in locators for thread safety
- optimize JavaScriptWaitManager for thread safety and reduce webdriver calls
- remove lazyLoadingTimeout parameter and replace with the same elementIdentificationTimeout

* limit number of threads for mac/safari

* fix ShadowDom SHAFT locator builder

* optimize driver init exception handling

-- handle extra edge-related exception
  • Loading branch information
MohabMohie authored Mar 23, 2024
1 parent a401f81 commit 5d88026
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 189 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/e2eTests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ jobs:
maven-version: 3.9.5
- name: Run tests
continue-on-error: true
run: mvn -e test "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=local" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=SAFARI" "-DgenerateAllureReportArchive=true" "-Dtest=${GLOBAL_TESTING_SCOPE}"
run: mvn -e test "-DsetThreadCount=1" "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=local" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=SAFARI" "-DgenerateAllureReportArchive=true" "-Dtest=${GLOBAL_TESTING_SCOPE}"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down Expand Up @@ -336,7 +336,7 @@ jobs:
chrome-version: stable
- name: Run tests
continue-on-error: true
run: mvn -e test "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=local" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=chrome" "-DheadlessExecution=true" "-DgenerateAllureReportArchive=true" "-Dtest=${GLOBAL_TESTING_SCOPE}"
run: mvn -e test "-DsetThreadCount=1" "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=local" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=chrome" "-DheadlessExecution=true" "-DgenerateAllureReportArchive=true" "-Dtest=${GLOBAL_TESTING_SCOPE}"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down Expand Up @@ -516,7 +516,7 @@ jobs:
maven-version: 3.9.5
- name: Run tests
continue-on-error: true
run: mvn -e test "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=browserstack" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=Safari" "-DbrowserStack.os=OS X" "-DbrowserStack.osVersion=Sonoma" "-DbrowserStack.browserVersion=17.0" "-DgenerateAllureReportArchive=true" "-Dtest=%regex[.*BrowserActionsTests.*], %regex[.*BigPageActionsTest.*]"
run: mvn -e test "-DsetThreadCount=1" "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=browserstack" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=Safari" "-DbrowserStack.os=OS X" "-DbrowserStack.osVersion=Sonoma" "-DbrowserStack.browserVersion=17.0" "-DgenerateAllureReportArchive=true" "-Dtest=%regex[.*BrowserActionsTests.*], %regex[.*BigPageActionsTest.*]"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down Expand Up @@ -649,7 +649,7 @@ jobs:
maven-version: 3.9.5
- name: Run tests
continue-on-error: true
run: mvn -e test "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=browserstack" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=Safari" "-DmaximumPerformanceMode=1" "-DbrowserStack.os=OS X" "-DbrowserStack.osVersion=Sonoma" "-DbrowserStack.browserVersion=17.0" -DgenerateAllureReportArchive="true" -Dtest="%regex[.*CucumberTests.*]"
run: mvn -e test "-DsetThreadCount=1" "-DretryMaximumNumberOfAttempts=1" "-DexecutionAddress=browserstack" "-DtargetOperatingSystem=MAC" "-DtargetBrowserName=Safari" "-DmaximumPerformanceMode=1" "-DbrowserStack.os=OS X" "-DbrowserStack.osVersion=Sonoma" "-DbrowserStack.browserVersion=17.0" -DgenerateAllureReportArchive="true" -Dtest="%regex[.*CucumberTests.*]"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,23 @@ private void createNewLocalDriverInstance(DriverType driverType, int retryAttemp
}
ReportManager.log(initialLog.replace("Attempting to run locally on", "Successfully Opened") + ".");
} catch (Exception exception) {
if (exception.getMessage().contains("cannot create default profile directory")) {
String message = exception.getMessage();
if (message.contains("cannot create default profile directory")) {
// this exception happens when the profile directory is not correct, very specific case
// should fail immediately
failAction("Failed to create new Browser Session", exception);
}
if (exception.getMessage().contains("Failed to initialize BiDi Mapper")) {
} else if (message.contains("DevToolsActivePort file doesn't exist")) {
// this exception was observed with `Windows_Edge_Local` pipeline to happen randomly
// suggested fix as per titus fortner: https://bugs.chromium.org/p/chromedriver/issues/detail?id=4403#c35

var chOptions = optionsManager.getChOptions();
chOptions.addArguments("--remote-debugging-pipe");
optionsManager.setChOptions(chOptions);

var edOptions = optionsManager.getEdOptions();
edOptions.addArguments("--remote-debugging-pipe");
optionsManager.setEdOptions(edOptions);
} else if (message.contains("Failed to initialize BiDi Mapper")) {
// this exception happens in some corner cases where the capabilities are not compatible with BiDi mode
// should force disable BiDi and try again
SHAFT.Properties.platform.set().enableBiDi(false);
Expand All @@ -326,11 +337,10 @@ private void createNewLocalDriverInstance(DriverType driverType, int retryAttemp
chOptions.setCapability("webSocketUrl", SHAFT.Properties.platform.enableBiDi());
optionsManager.setChOptions(chOptions);

var EdOptions = optionsManager.getEdOptions();
EdOptions.setCapability("webSocketUrl", SHAFT.Properties.platform.enableBiDi());
optionsManager.setEdOptions(EdOptions);
}
if (driverType.equals(DriverType.SAFARI) && Throwables.getRootCause(exception).getMessage().toLowerCase().contains("safari instance is already paired with another webdriver session")) {
var edOptions = optionsManager.getEdOptions();
edOptions.setCapability("webSocketUrl", SHAFT.Properties.platform.enableBiDi());
optionsManager.setEdOptions(edOptions);
} else if (driverType.equals(DriverType.SAFARI) && Throwables.getRootCause(exception).getMessage().toLowerCase().contains("safari instance is already paired with another webdriver session")) {
//this issue happens when running locally via safari/mac platform
// sample failure can be found here: https://github.com/ShaftHQ/SHAFT_ENGINE/actions/runs/4527911969/jobs/7974202314#step:4:46621
// attempting blind fix by trying to quit existing safari instances if any
Expand All @@ -339,16 +349,7 @@ private void createNewLocalDriverInstance(DriverType driverType, int retryAttemp
} catch (Throwable throwable) {
// ignore
}
}
// attempting blind fix by trying to quit existing driver if any
try {
driver.quit();
} catch (Throwable throwable) {
// ignore
} finally {
setDriver(null);
}
if (exception.getMessage().contains("java.util.concurrent.TimeoutException")) {
} else if (exception.getMessage().contains("java.util.concurrent.TimeoutException")) {
// this happens in case an auto closable BiDi session was left hanging
// the default timeout is 30 seconds, so this wait will waste 26 and the following will waste 5 more
// the desired effect will be to wait for the bidi session to timeout
Expand All @@ -358,6 +359,15 @@ private void createNewLocalDriverInstance(DriverType driverType, int retryAttemp
//do nothing
}
}
// attempting blind fix by trying to quit existing driver if any
try {
driver.quit();
} catch (Throwable throwable) {
// ignore
} finally {
setDriver(null);
}
// evaluating retry attempts
if (retryAttempts > 0) {
try {
Thread.sleep(5000);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/shaft/gui/browser/BrowserActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ public BrowserActions navigateToURL(String targetUrl, WindowType windowType) {
*/
public BrowserActions navigateToURL(String targetUrl, String targetUrlAfterRedirection) {
//reset scope in case user was stuck inside an iFrame
LocatorBuilder.setIFrameLocator(null);
ShadowLocatorBuilder.shadowDomLocator = null;
LocatorBuilder.getIFrameLocator().remove();
ShadowLocatorBuilder.shadowDomLocator.remove();

String modifiedTargetUrl = targetUrl;
var baseUrl = SHAFT.Properties.web.baseURL();
Expand Down
163 changes: 33 additions & 130 deletions src/main/java/com/shaft/gui/browser/internal/JavaScriptWaitManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,25 @@

import com.shaft.driver.SHAFT;
import com.shaft.driver.internal.DriverFactory.DriverFactoryHelper;
import com.shaft.driver.internal.DriverFactory.SynchronizationManager;
import com.shaft.tools.internal.support.JavaScriptHelper;
import com.shaft.tools.io.internal.ReportManagerHelper;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Arrays;

public class JavaScriptWaitManager {
private static final String TARGET_DOCUMENT_READY_STATE = "complete";
private static final ThreadLocal<WebDriver> jsWaitDriver = new ThreadLocal<>();
private static final int delayBetweenPolls = 20; // milliseconds
private static int WAIT_DURATION_INTEGER;
private static JavascriptExecutor jsExec;
private static final ThreadLocal<JavascriptExecutor> jsExec = new ThreadLocal<>();

private JavaScriptWaitManager() {
throw new IllegalStateException("Utility class");
}

private static void setDriver(WebDriver driver) {
jsWaitDriver.set(driver);
jsExec = (JavascriptExecutor) jsWaitDriver.get();
WAIT_DURATION_INTEGER = SHAFT.Properties.timeouts.lazyLoadingTimeout();
jsExec.set((JavascriptExecutor) jsWaitDriver.get());
}

/**
Expand All @@ -38,9 +31,9 @@ public static void waitForLazyLoading(WebDriver driver) {
if (SHAFT.Properties.timeouts.waitForLazyLoading()
&& !DriverFactoryHelper.isMobileNativeExecution()) {
ArrayList<Thread> lazyLoadingThreads = new ArrayList<>();
lazyLoadingThreads.add(Thread.ofVirtual().start(JavaScriptWaitManager::waitForJQueryLoadIfDefined));
lazyLoadingThreads.add(Thread.ofVirtual().start(JavaScriptWaitManager::waitForAngularIfDefined));
lazyLoadingThreads.add(Thread.ofVirtual().start(JavaScriptWaitManager::waitForJSLoadIfDefined));
lazyLoadingThreads.add(Thread.ofVirtual().start(JavaScriptWaitManager::waitForJQuery));
lazyLoadingThreads.add(Thread.ofVirtual().start(JavaScriptWaitManager::waitForAngular));
lazyLoadingThreads.add(Thread.ofVirtual().start(JavaScriptWaitManager::waitForDocumentReadyState));
lazyLoadingThreads.forEach(thread -> {
try {
thread.join();
Expand All @@ -51,129 +44,39 @@ public static void waitForLazyLoading(WebDriver driver) {
}
}

private static void waitForJQueryLoadIfDefined() {
try {
Boolean jQueryDefined = (Boolean) jsExec.executeScript("return typeof jQuery != 'undefined'");
if (Boolean.TRUE.equals(jQueryDefined)) {
ExpectedCondition<Boolean> jQueryLoad = null;
private static void waitForDocumentReadyState() {
new SynchronizationManager(jsWaitDriver.get()).fluentWait().until(f -> {
try {
// Wait for jQuery to load
jQueryLoad = driver -> ((Long) ((JavascriptExecutor) jsWaitDriver.get())
.executeScript("return jQuery.active") == 0);
} catch (NullPointerException e) {
// do nothing
var ready = Arrays.asList("loaded", "complete");
return ready.contains(jsExec.get().executeScript(JavaScriptHelper.DOCUMENT_READY_STATE.getValue()).toString());
} catch (Exception exception) {
// force return in case of unexpected exception
return true;
}
// Get JQuery is Ready
boolean jqueryReady = (Boolean) jsExec.executeScript("return jQuery.active==0");

if (!jqueryReady) {
// Wait JQuery until it is Ready!
int tryCounter = 0;
while ((!jqueryReady) && (tryCounter < 5)) {
try {
// Wait for jQuery to load
(new WebDriverWait(jsWaitDriver.get(), Duration.ofSeconds(WAIT_DURATION_INTEGER))).until(jQueryLoad);
} catch (NullPointerException e) {
// do nothing
}
sleep();
tryCounter++;
jqueryReady = (Boolean) jsExec.executeScript("return jQuery.active == 0");
}
}
}
} catch (Throwable throwable) {
// do nothing
}
});
}

private static void waitForAngularLoad() {
JavascriptExecutor jsExec = (JavascriptExecutor) jsWaitDriver.get();

String angularReadyScript = "return angular.element(document).injector().get('$http').pendingRequests.length === 0";

// Wait for ANGULAR to load
ExpectedCondition<Boolean> angularLoad = driver -> Boolean
.valueOf(((JavascriptExecutor) Objects.requireNonNull(driver)).executeScript(angularReadyScript).toString());

// Get Angular is Ready
boolean angularReady = Boolean.parseBoolean(jsExec.executeScript(angularReadyScript).toString());

if (!angularReady) {
// Wait ANGULAR until it is Ready!
int tryCounter = 0;
while ((!angularReady) && (tryCounter < 5)) {
// Wait for Angular to load
(new WebDriverWait(jsWaitDriver.get(), Duration.ofSeconds(WAIT_DURATION_INTEGER))).until(angularLoad);
// ExpectedCondition<Boolean> finalAngularLoad = angularLoad;
// (new WebDriverWait(jsWaitDriver.get(), WAIT_DURATION)).until(waitDriver-> finalAngularLoad);
// More Wait for stability (Optional)
sleep();
tryCounter++;
angularReady = Boolean.parseBoolean(jsExec.executeScript(angularReadyScript).toString());
}
}
}

private static void waitForJSLoadIfDefined() {
try {
JavascriptExecutor jsExec = (JavascriptExecutor) jsWaitDriver.get();

// Wait for Javascript to load
ExpectedCondition<Boolean> jsLoad = driver -> ((JavascriptExecutor) jsWaitDriver.get())
.executeScript(JavaScriptHelper.DOCUMENT_READYSTATE.getValue()).toString().trim()
.equalsIgnoreCase(TARGET_DOCUMENT_READY_STATE);

// Get JS is Ready
boolean jsReady = jsExec.executeScript(JavaScriptHelper.DOCUMENT_READYSTATE.getValue()).toString().trim()
.equalsIgnoreCase(TARGET_DOCUMENT_READY_STATE);

// Wait Javascript until it is Ready!
if (!jsReady) {
// Wait JS until it is Ready!
int tryCounter = 0;
while ((!jsReady) && (tryCounter < 5)) {
// Wait for Javascript to load
try {
(new WebDriverWait(jsWaitDriver.get(), Duration.ofSeconds(WAIT_DURATION_INTEGER))).until(jsLoad);
} catch (org.openqa.selenium.TimeoutException e) {
//do nothing
//TODO: confirm that this fixed the timeout issue on the grid
}
// More Wait for stability (Optional)
sleep();
tryCounter++;
jsReady = jsExec.executeScript(JavaScriptHelper.DOCUMENT_READYSTATE.getValue()).toString().trim()
.equalsIgnoreCase(TARGET_DOCUMENT_READY_STATE);
private static void waitForJQuery() {
new SynchronizationManager(jsWaitDriver.get()).fluentWait().until(f -> {
try {
return Long.parseLong(jsExec.get().executeScript(JavaScriptHelper.JQUERY_ACTIVE_STATE.getValue()).toString()) == 0;
} catch (Exception exception) {
// force return in case of unexpected exception
// org.openqa.selenium.JavascriptException: javascript error: jQuery is not defined
return true;
}
}
} catch (Throwable throwable) {
// do nothing
}
});
}

private static void waitForAngularIfDefined() {
try {
Boolean angularDefined = !((Boolean) jsExec.executeScript("return window.angular === undefined"));
if (Boolean.TRUE.equals(angularDefined)) {
Boolean angularInjectorDefined = !((Boolean) jsExec
.executeScript("return angular.element(document).injector() === undefined"));

if (Boolean.TRUE.equals(angularInjectorDefined)) {
waitForAngularLoad();
}
private static void waitForAngular() {
new SynchronizationManager(jsWaitDriver.get()).fluentWait().until(f -> {
try {
return Long.parseLong(jsExec.get().executeScript(JavaScriptHelper.ANGULAR_READY_STATE.getValue()).toString()) == 0;
} catch (Exception exception) {
// force return in case of unexpected exception
// org.openqa.selenium.JavascriptException: javascript error: angular is not defined
return true;
}
} catch (Throwable throwable) {
// do nothing
}
}

private static void sleep() {
try {
Thread.sleep(JavaScriptWaitManager.delayBetweenPolls);
} catch (Exception e) {
ReportManagerHelper.logDiscrete(e);
// InterruptedException
}
});
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/shaft/gui/element/ElementActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ public ElementActions submitFormUsingJavaScript(By elementLocator) {
public ElementActions switchToIframe(By elementLocator) {
try {
var elementInformation = ElementInformation.fromList(ElementActionsHelper.identifyUniqueElement(driver, elementLocator));
LocatorBuilder.setIFrameLocator(elementInformation.getLocator());
LocatorBuilder.getIFrameLocator().set(elementInformation.getLocator());
// note to self: remove elementLocator in case of bug in screenshot manager
driver.switchTo().frame(elementInformation.getFirstElement());
boolean discreetLoggingState = ReportManagerHelper.getDiscreteLogging();
Expand All @@ -693,7 +693,7 @@ public ElementActions switchToIframe(By elementLocator) {
public ElementActions switchToDefaultContent() {
try {
driver.switchTo().defaultContent();
LocatorBuilder.setIFrameLocator(null);
LocatorBuilder.getIFrameLocator().remove();
boolean discreetLoggingState = ReportManagerHelper.getDiscreteLogging();
ReportManagerHelper.setDiscreteLogging(true);
ElementActionsHelper.passAction(driver, null, Thread.currentThread().getStackTrace()[1].getMethodName(), null, null, null);
Expand Down
Loading

0 comments on commit 5d88026

Please sign in to comment.