Stale element not found, is this relate to using same Object?

@jirayu.s

In a previous post I recommended you to disable Smart Wait.

Now I have changed my opinion.

Smart Wait is the only pragmatic solution for you to pass the sign-in procedure of Azure DevOps.


I found that you are trying to automate the sign-in procedure into Microsoft Azure DevOps. I created my Azure DevOps account, and tried to write a Katalon Test Case that automates signing-in to it. My code is as follows:

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import internal.GlobalVariable

TestObject makeTestObject(String id, String xpathExpression) {
    TestObject tObj = new TestObject(id)
    tObj.addProperty("xpath", ConditionType.EQUALS, xpathExpression)
    return tObj
}

String url = "https://dev.azure.com/${GlobalVariable.ACCOUNT}"
WebUI.openBrowser(url)
WebUI.setViewPortSize(800, 800)

TestObject loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
WebUI.waitForElementClickable(loginfmt, 20)
WebUI.sendKeys(loginfmt, GlobalVariable.EMAIL)

TestObject nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
WebUI.waitForElementClickable(nextButton, 20)
WebUI.click(nextButton)

TestObject passwd = makeTestObject("Passwd", "//input[@name='passwd']")
WebUI.waitForElementClickable(passwd, 20)
WebUI.sendKeys(passwd, GlobalVariable.PASSWD)

TestObject signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
WebUI.waitForElementClickable(signinButton, 20)
WebUI.click(signinButton)

TestObject yesButton = makeTestObject("Yes", "//input[@id='idSIButton9']")
WebUI.waitForElementClickable(signinButton, 20)
WebUI.click(signinButton)

WebUI.closeBrowser()

With Smart Wait enabled, this code passed.

With Smart Wait disabled, this code failed with Stale Element Reference Exceptions at several statements.


I looked at the HTML of the Azore DevOps Sign-in page and found that it is heavily JavaScript-driven.

I found that the page is driven by a rich set of JavaScript codes. And your test case script must keep in sync with the JavaScript codes running in the browser.

However, none of WebUI.waitFor*** keywords are capable to synchronize with the JavaScript in browser. Therefore we can not solve the “stale element reference” problem using any of WebUI.*** keywords. I think it is pointless to discuss how to utilize WebUI.waitFor*** and WebUI.delay(n) keywords for this case.

As @Brandon_Hein told at Stale element not found, is this relate to using same Object? - #96 by Brandon_Hein , the Smart Wait talks to the JavaScript running inside browser and moinitor the modifications in the DOM. A Test Case script can get in sync with JavaScript runnging inside browser by enabling Smart Wait.

or some may just listen to @Russ_Thomas advices and write proper waits and locators by using javascript (or everything else)
just saying…

sorry @Brandon_Hein for ‘ignoring’ your recent posts.
and welcome back.
grab some popcorn and enjoy the history of this topic, read everything please for now.
judge later.

No worries, good to be back in the mix :slight_smile:

enjoy the kookoo forum :slight_smile:

The following is a variation of my Test Case where I enable and disable the Smart Wait by code. I think that explicit code is better than by config.The code clearly reminds me that I need the Smart Wait here.

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import internal.GlobalVariable

/**
 * automate signing-in to Microsoft Azure DevOps with Smart Wait
 */

TestObject makeTestObject(String id, String xpathExpression) {
    TestObject tObj = new TestObject(id)
    tObj.addProperty("xpath", ConditionType.EQUALS, xpathExpression)
    return tObj
}

// enable Smart Wait explicitly
WebUI.enableSmartWait()

String url = "https://dev.azure.com/${GlobalVariable.ACCOUNT}"
WebUI.openBrowser(url)
WebUI.setViewPortSize(800, 800)

TestObject loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
WebUI.sendKeys(loginfmt, GlobalVariable.EMAIL)

TestObject nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
WebUI.click(nextButton)

TestObject passwd = makeTestObject("Passwd", "//input[@name='passwd']")
WebUI.sendKeys(passwd, GlobalVariable.PASSWD)

TestObject signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
WebUI.click(signinButton)

TestObject yesButton = makeTestObject("Yes", "//input[@id='idSIButton9']")
WebUI.click(signinButton)

WebUI.closeBrowser()

WebUI.disableSmartWait()

I no longer need any calls to WebUI.waitFor*** and WebUI.delay in the block enclosed by a pair of enableSmartWait() and disableSmartWait().

Cuckoo!

1 Like

This time of experiment impressed me how poor the Katalon documentation for Smart Wait is.

The following is the only sentence in the Katalon documentation what the Smart Wait is:

The Smart Wait function tackles the timing issue of automated web testing by automatically waiting for all front-end processes of a page to complete before taking further test steps.

Is that all? — yes, that’s all.

I find this sentence not enough. This short sentence tells me nothing how the Smart Wait works (it talks to the JavaScript, it monitors any modifications in the DOM, …), so that when to use it. This description impressed me that the Smart Wait is something evil that does something unexplained. It would make my test scripts enchanted. I would loose control over my codes. I should not trust anything that I do not understand. So I had made up my mind not to touch it.

This time I experimented automating the sign-in procedure of Microsoft DevOps which is really JavaScript-driven. I realized that this is the case where Smart Wait shines. The post by @Brandon_Hein and @Russ_Thomas helped me a lot, of course.

I think, Katalon should improve the doc; Katalon should take DevOps Sign-in procedure as an example, should describe @jirayu.s’ case as a use case scenario, what is diffucult there (stale element reference), and how Smart Wat helps. It deserves an article on its own.

@Elly_Tran

2 Likes

In terms of a specific implementation, I would understand if they didn’t want to share details, since the Smart Wait feature is proprietary and a selling point for the Studio overall. But I agree, it would be nice to know at least what the methodology is behind it. From my research a couple of years ago, it involved a couple of pieces, but the main one was using a MutationObserver in JavaScript to watch a page change. This is something I’ve replicated myself, coincidentally, and have used with varying success. I don’t think this is the only thing Smart Wait does, but it’s part of it. Here’s the one I created:

if (typeof observer === 'undefined') {
   window.domModifiedTime = Date.now();
   var targetNode = document.querySelector('body');
   var config = {
      attributes: true,
      childList: true,
      subtree: true
   };
   var callback = () => {
      window.domModifiedTime = Date.now();
   };
   var observer = new MutationObserver(callback);
   observer.observe(targetNode, config);
}

After that is injected, you just wait for the domModifiedTime to stop changing, indicating that the page is static.

Again, I’ve had spotty results with this. I think this unreliability is due to varying page loading times and the polling rate of your wait condition. I’ve had it wait perfectly for some page loads, and completely “randomly” for others.

Could you give me a sample Katalon Test Case that does, by WebUI.executeJavascript() of course, injecting this piece of JavaScript and monitoring the dom changes? I want to extend my exeriments so that I employ your methodology instead of Smart Wait.

I cannot, I don’t use Katalon Studio, but here’s a sample using a Java method. You should be able to copy paste this somewhere and have it work out of box:

    /**
     * Uses a MutationObserver (JavaScript) to wait for the DOM to stop changing. "Changing" can include addition/removal/modification of ANY elements in the DOM
     */
    public static void waitForDomModifications() {
        JavascriptExecutor javascriptExecutor = JavaScriptUtil.getJavascriptExecutor();
        javascriptExecutor.executeScript("if(typeof observer === 'undefined') { window.domModifiedTime = Date.now(); var targetNode = document.querySelector('body'); var config = { attributes: true, childList: true, subtree: true }; var callback = () => { window.domModifiedTime = Date.now(); }; var observer = new MutationObserver(callback); observer.observe(targetNode, config); }");
        long modifiedTime = 0;
        long start = System.currentTimeMillis();
        while (modifiedTime != (long) javascriptExecutor.executeScript("return window.domModifiedTime") && System.currentTimeMillis() - start < 30000) {
            modifiedTime = (long) javascriptExecutor.executeScript("return window.domModifiedTime");
            _wait(300);
        }
    }

You’ll just need to get a JavascriptExecutor instance from the Katalon libs, however that is done. I have a 300 ms “polling” rate with the _wait(300);, you’d have to use WebUI.delay() probably.

1 Like

OK. Enough. I will try.
Thank you for your contribution.

I found that WebUI.enableSmartWait() does NOT work.

I have a Test Case:

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import internal.GlobalVariable

/**
 * automate signing-in to Microsoft Azure DevOps with Smart Wait
 */

TestObject makeTestObject(String id, String xpathExpression) {
    TestObject tObj = new TestObject(id)
    tObj.addProperty("xpath", ConditionType.EQUALS, xpathExpression)
    return tObj
}

// enable Smart Wait explicitly
WebUI.enableSmartWait()

String url = "https://dev.azure.com/${GlobalVariable.ACCOUNT}"
WebUI.openBrowser(url)
WebUI.setViewPortSize(800, 800)

TestObject loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
WebUI.sendKeys(loginfmt, GlobalVariable.EMAIL)

TestObject nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
WebUI.click(nextButton)

TestObject passwd = makeTestObject("Passwd", "//input[@name='passwd']")
WebUI.sendKeys(passwd, GlobalVariable.PASSWD)

TestObject signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
WebUI.click(signinButton)

TestObject yesButton = makeTestObject("Yes", "//input[@id='idSIButton9']")
WebUI.click(yesButton)

WebUI.closeBrowser()

WebUI.disableSmartWait()

I examined 2 cases
a) Project > Settings > Execution > WebUI > Default Smart Wait: Enabled → the test case passed
b) Project > Settings > Execution > WebUI > Default Smart Wait: Disabled → the test case failed, with the following error:

2023-10-13 22:08:38.731 INFO  c.k.katalon.core.main.TestCaseExecutor   - --------------------
2023-10-13 22:08:38.734 INFO  c.k.katalon.core.main.TestCaseExecutor   - START Test Cases/TC2
2023-10-13 22:08:39.324 DEBUG testcase.TC2                             - 1: enableSmartWait()
2023-10-13 22:08:39.630 DEBUG testcase.TC2                             - 2: url = https://dev.azure.com/$GlobalVariable.ACCOUNT
2023-10-13 22:08:39.855 DEBUG testcase.TC2                             - 3: openBrowser(url)
2023-10-13 22:08:39.926 INFO  c.k.k.core.webui.driver.DriverFactory    - Starting 'Chrome' driver
10 13, 2023 10:08:39 午後 org.openqa.selenium.remote.DesiredCapabilities chrome
情報: Using `new ChromeOptions()` is preferred to `DesiredCapabilities.chrome()`
2023-10-13 22:08:39.983 INFO  c.k.k.core.webui.driver.DriverFactory    - Action delay is set to 0 milliseconds
Starting ChromeDriver 118.0.5993.70 (e52f33f30b91b4ddfad649acddc39ab570473b86-refs/branch-heads/5993@{#1216}) on port 7920
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.
10 13, 2023 10:08:43 午後 org.openqa.selenium.remote.ProtocolHandshake createSession
情報: Detected dialect: W3C
2023-10-13 22:08:43.518 INFO  c.k.k.core.webui.driver.DriverFactory    - sessionId = 9bd2c17e887fe3aa8e3cb2d39edce991
2023-10-13 22:08:43.564 INFO  c.k.k.core.webui.driver.DriverFactory    - browser = Chrome 118.0.0.0
2023-10-13 22:08:43.565 INFO  c.k.k.core.webui.driver.DriverFactory    - platform = Mac OS X
2023-10-13 22:08:43.566 INFO  c.k.k.core.webui.driver.DriverFactory    - seleniumVersion = 3.141.59
2023-10-13 22:08:43.569 INFO  c.k.k.core.webui.driver.DriverFactory    - proxyInformation = ProxyInformation { proxyOption=NO_PROXY, proxyServerType=HTTP, username=, password=********, proxyServerAddress=, proxyServerPort=0, executionList="", isApplyToDesiredCapabilities=true }
2023-10-13 22:08:47.859 DEBUG testcase.TC2                             - 4: setViewPortSize(800, 800)
2023-10-13 22:08:48.087 DEBUG testcase.TC2                             - 5: loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
2023-10-13 22:08:48.089 DEBUG testcase.TC2                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-13 22:08:48.104 DEBUG testcase.TC2                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-13 22:08:48.115 DEBUG testcase.TC2                             - 3: return tObj
2023-10-13 22:08:48.117 DEBUG testcase.TC2                             - 6: sendKeys(loginfmt, EMAIL)
2023-10-13 22:08:49.627 DEBUG testcase.TC2                             - 7: nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
2023-10-13 22:08:49.629 DEBUG testcase.TC2                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-13 22:08:49.630 DEBUG testcase.TC2                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-13 22:08:49.631 DEBUG testcase.TC2                             - 3: return tObj
2023-10-13 22:08:49.633 DEBUG testcase.TC2                             - 8: click(nextButton)
2023-10-13 22:08:49.825 DEBUG testcase.TC2                             - 9: passwd = makeTestObject("Passwd", "//input[@name='passwd']")
2023-10-13 22:08:49.829 DEBUG testcase.TC2                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-13 22:08:49.833 DEBUG testcase.TC2                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-13 22:08:49.834 DEBUG testcase.TC2                             - 3: return tObj
2023-10-13 22:08:49.838 DEBUG testcase.TC2                             - 10: sendKeys(passwd, PASSWD)
2023-10-13 22:08:49.996 DEBUG testcase.TC2                             - 11: signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
2023-10-13 22:08:49.998 DEBUG testcase.TC2                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-13 22:08:50.000 DEBUG testcase.TC2                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-13 22:08:50.001 DEBUG testcase.TC2                             - 3: return tObj
2023-10-13 22:08:50.002 DEBUG testcase.TC2                             - 12: click(signinButton)
2023-10-13 22:08:52.312 ERROR c.k.k.core.keyword.internal.KeywordMain  - ❌ Unable to click on object 'SignIn' (Root cause: com.kms.katalon.core.exception.StepFailedException: Unable to click on object 'SignIn'
	at com.kms.katalon.core.webui.keyword.internal.WebUIKeywordMain.stepFailed(WebUIKeywordMain.groovy:64)
	at com.kms.katalon.core.webui.keyword.internal.WebUIKeywordMain.runKeyword(WebUIKeywordMain.groovy:26)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.click(ClickKeyword.groovy:74)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.execute(ClickKeyword.groovy:40)
	at com.kms.katalon.core.keyword.internal.KeywordExecutor.executeKeywordForPlatform(KeywordExecutor.groovy:74)
	at com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords.click(WebUiBuiltInKeywords.groovy:620)
	at com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords$click$3.call(Unknown Source)
	at TC2.run(TC2:33)
	at com.kms.katalon.core.main.ScriptEngine.run(ScriptEngine.java:194)
	at com.kms.katalon.core.main.ScriptEngine.runScriptAsRawText(ScriptEngine.java:119)
	at com.kms.katalon.core.main.TestCaseExecutor.runScript(TestCaseExecutor.java:448)
	at com.kms.katalon.core.main.TestCaseExecutor.doExecute(TestCaseExecutor.java:439)
	at com.kms.katalon.core.main.TestCaseExecutor.processExecutionPhase(TestCaseExecutor.java:418)
	at com.kms.katalon.core.main.TestCaseExecutor.accessMainPhase(TestCaseExecutor.java:410)
	at com.kms.katalon.core.main.TestCaseExecutor.execute(TestCaseExecutor.java:285)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:144)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:135)
	at com.kms.katalon.core.main.TestCaseMain$runTestCase$0.call(Unknown Source)
	at TempTestCase1697202515365.run(TempTestCase1697202515365.groovy:25)
Caused by: org.openqa.selenium.StaleElementReferenceException: stale element reference: stale element not found
  (Session info: chrome=118.0.5993.70)
For documentation on this error, please visit: https://www.seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: '3.141.59', revision: 'e82be7d358', time: '2018-11-14T08:25:53'
System info: host: 'KAZUAKInoMacBook-Air-2.local', ip: 'fe80:0:0:0:4dc:d889:a9a4:e532%en0', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '12.6.8', java.version: '1.8.0_362'
Driver info: com.kms.katalon.selenium.driver.CChromeDriver
Capabilities {acceptInsecureCerts: false, browserName: chrome, browserVersion: 118.0.5993.70, chrome: {chromedriverVersion: 118.0.5993.70 (e52f33f30b91..., userDataDir: /var/folders/7m/lm7d6nx51kj...}, fedcm:accounts: true, goog:chromeOptions: {debuggerAddress: localhost:58341}, javascriptEnabled: true, networkConnectionEnabled: false, pageLoadStrategy: normal, platform: MAC, platformName: MAC, proxy: Proxy(), setWindowRect: true, strictFileInteractability: false, timeouts: {implicit: 0, pageLoad: 300000, script: 30000}, unhandledPromptBehavior: dismiss and notify, webauthn:extension:credBlob: true, webauthn:extension:largeBlob: true, webauthn:extension:minPinLength: true, webauthn:extension:prf: true, webauthn:virtualAuthenticators: true}
Session ID: 9bd2c17e887fe3aa8e3cb2d39edce991
	at org.openqa.selenium.remote.http.W3CHttpResponseCodec.createException(W3CHttpResponseCodec.java:187)
	at org.openqa.selenium.remote.http.W3CHttpResponseCodec.decode(W3CHttpResponseCodec.java:122)
	at org.openqa.selenium.remote.http.W3CHttpResponseCodec.decode(W3CHttpResponseCodec.java:49)
	at org.openqa.selenium.remote.HttpCommandExecutor.execute(HttpCommandExecutor.java:158)
	at org.openqa.selenium.remote.service.DriverCommandExecutor.execute(DriverCommandExecutor.java:83)
	at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:552)
	at com.kms.katalon.selenium.driver.CChromeDriver.execute(CChromeDriver.java:19)
	at org.openqa.selenium.remote.RemoteWebElement.execute(RemoteWebElement.java:285)
	at org.openqa.selenium.remote.RemoteWebElement.click(RemoteWebElement.java:84)
	at org.openqa.selenium.support.events.EventFiringWebDriver$EventFiringWebElement.lambda$new$0(EventFiringWebDriver.java:404)
	at com.sun.proxy.$Proxy11.click(Unknown Source)
	at org.openqa.selenium.support.events.EventFiringWebDriver$EventFiringWebElement.click(EventFiringWebDriver.java:417)
	at org.openqa.selenium.WebElement$click.call(Unknown Source)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.clickUntilSuccessWithTimeout(ClickKeyword.groovy:81)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.clickUntilSuccessWithTimeout(ClickKeyword.groovy)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword$_click_closure1.doCall(ClickKeyword.groovy:67)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword$_click_closure1.call(ClickKeyword.groovy)
	at com.kms.katalon.core.webui.keyword.internal.WebUIKeywordMain.runKeyword(WebUIKeywordMain.groovy:20)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.click(ClickKeyword.groovy:74)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.execute(ClickKeyword.groovy:40)
	at com.kms.katalon.core.keyword.internal.KeywordExecutor.executeKeywordForPlatform(KeywordExecutor.groovy:74)
	at com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords.click(WebUiBuiltInKeywords.groovy:620)
	at com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords$click$3.call(Unknown Source)
	at Script1697145114209.run(Script1697145114209.groovy:33)
	... 11 more
)
2023-10-13 22:08:52.323 ERROR c.k.katalon.core.main.TestCaseExecutor   - ❌ Test Cases/TC2 FAILED.
Reason:
com.kms.katalon.core.exception.StepFailedException: Unable to click on object 'SignIn'
	at com.kms.katalon.core.webui.keyword.internal.WebUIKeywordMain.stepFailed(WebUIKeywordMain.groovy:64)
	at com.kms.katalon.core.webui.keyword.internal.WebUIKeywordMain.runKeyword(WebUIKeywordMain.groovy:26)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.click(ClickKeyword.groovy:74)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.execute(ClickKeyword.groovy:40)
	at com.kms.katalon.core.keyword.internal.KeywordExecutor.executeKeywordForPlatform(KeywordExecutor.groovy:74)
	at com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords.click(WebUiBuiltInKeywords.groovy:620)
	at com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords$click$3.call(Unknown Source)
	at TC2.run(TC2:33)
	at com.kms.katalon.core.main.ScriptEngine.run(ScriptEngine.java:194)
	at com.kms.katalon.core.main.ScriptEngine.runScriptAsRawText(ScriptEngine.java:119)
	at com.kms.katalon.core.main.TestCaseExecutor.runScript(TestCaseExecutor.java:448)
	at com.kms.katalon.core.main.TestCaseExecutor.doExecute(TestCaseExecutor.java:439)
	at com.kms.katalon.core.main.TestCaseExecutor.processExecutionPhase(TestCaseExecutor.java:418)
	at com.kms.katalon.core.main.TestCaseExecutor.accessMainPhase(TestCaseExecutor.java:410)
	at com.kms.katalon.core.main.TestCaseExecutor.execute(TestCaseExecutor.java:285)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:144)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:135)
	at com.kms.katalon.core.main.TestCaseMain$runTestCase$0.call(Unknown Source)
	at TempTestCase1697202515365.run(TempTestCase1697202515365.groovy:25)
Caused by: org.openqa.selenium.StaleElementReferenceException: stale element reference: stale element not found
  (Session info: chrome=118.0.5993.70)
For documentation on this error, please visit: https://www.seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: '3.141.59', revision: 'e82be7d358', time: '2018-11-14T08:25:53'
System info: host: 'KAZUAKInoMacBook-Air-2.local', ip: 'fe80:0:0:0:4dc:d889:a9a4:e532%en0', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '12.6.8', java.version: '1.8.0_362'
Driver info: com.kms.katalon.selenium.driver.CChromeDriver
Capabilities {acceptInsecureCerts: false, browserName: chrome, browserVersion: 118.0.5993.70, chrome: {chromedriverVersion: 118.0.5993.70 (e52f33f30b91..., userDataDir: /var/folders/7m/lm7d6nx51kj...}, fedcm:accounts: true, goog:chromeOptions: {debuggerAddress: localhost:58341}, javascriptEnabled: true, networkConnectionEnabled: false, pageLoadStrategy: normal, platform: MAC, platformName: MAC, proxy: Proxy(), setWindowRect: true, strictFileInteractability: false, timeouts: {implicit: 0, pageLoad: 300000, script: 30000}, unhandledPromptBehavior: dismiss and notify, webauthn:extension:credBlob: true, webauthn:extension:largeBlob: true, webauthn:extension:minPinLength: true, webauthn:extension:prf: true, webauthn:virtualAuthenticators: true}
Session ID: 9bd2c17e887fe3aa8e3cb2d39edce991
	at org.openqa.selenium.remote.http.W3CHttpResponseCodec.createException(W3CHttpResponseCodec.java:187)
	at org.openqa.selenium.remote.http.W3CHttpResponseCodec.decode(W3CHttpResponseCodec.java:122)
	at org.openqa.selenium.remote.http.W3CHttpResponseCodec.decode(W3CHttpResponseCodec.java:49)
	at org.openqa.selenium.remote.HttpCommandExecutor.execute(HttpCommandExecutor.java:158)
	at org.openqa.selenium.remote.service.DriverCommandExecutor.execute(DriverCommandExecutor.java:83)
	at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:552)
	at com.kms.katalon.selenium.driver.CChromeDriver.execute(CChromeDriver.java:19)
	at org.openqa.selenium.remote.RemoteWebElement.execute(RemoteWebElement.java:285)
	at org.openqa.selenium.remote.RemoteWebElement.click(RemoteWebElement.java:84)
	at org.openqa.selenium.support.events.EventFiringWebDriver$EventFiringWebElement.lambda$new$0(EventFiringWebDriver.java:404)
	at com.sun.proxy.$Proxy11.click(Unknown Source)
	at org.openqa.selenium.support.events.EventFiringWebDriver$EventFiringWebElement.click(EventFiringWebDriver.java:417)
	at org.openqa.selenium.WebElement$click.call(Unknown Source)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.clickUntilSuccessWithTimeout(ClickKeyword.groovy:81)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword.clickUntilSuccessWithTimeout(ClickKeyword.groovy)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword$_click_closure1.doCall(ClickKeyword.groovy:67)
	at com.kms.katalon.core.webui.keyword.builtin.ClickKeyword$_click_closure1.call(ClickKeyword.groovy)
	at com.kms.katalon.core.webui.keyword.internal.WebUIKeywordMain.runKeyword(WebUIKeywordMain.groovy:20)
	... 17 more

2023-10-13 22:08:52.351 INFO  c.k.katalon.core.main.TestCaseExecutor   - END Test Cases/TC2

This error proves that calling the WebUI.enableSmartWait() has no effect. It is not equivalent to configuring Default Smart Wait to be Enabled.

@duyluong

1 Like

Taking Brandon_Hein’s idea, I tried to implement a “generic wait” which can cope with DOM-modifications by JavaScript, which is alternative to the katlaon-built-in Smart Wait. See this GitHub repository.

I wrote a Groovy class:

package com.kazurayam.ks

import org.openqa.selenium.JavascriptExecutor
import org.openqa.selenium.WebDriver

public class WaitHelpers {

    /**
     * wait for the DOM to stop changing.
     * Uses a MutationObserver (JavaScript) to be notified of DOM modifications.
     * "Changing" can include addition/removal/modification of any elements in the DOM
     */
    public static void waitForDomModificationsToCease(WebDriver driver) {

        JavascriptExecutor jsx = (JavascriptExecutor)driver;
        jsx.executeScript("""
if (typeof observer === 'undefined') {
    window.domModifiedTime = Date.now();
    let targetNode = document.querySelector('body');
    let config = {
        attributes: true,
        childList: true,
        subtree: true
    };
    let callback = () => {
        window.domModifiedTime = Date.now();
    };
    let observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
}
""");
        double modifiedTime = 0;
        double start = System.currentTimeMillis();
        while (modifiedTime != (Double)jsx.executeScript("return window.domModifiedTime") && System.currentTimeMillis() - start < 30000) {
            println ">>> modifiedTime=" + modifiedTime;
            println "<<< jsx returned window.domModifiedTime=" + jsx.executeScript("return window.domModifiedTime");
            modifiedTime = (Double)jsx.executeScript("return window.domModifiedTime");
            Thread.sleep(300);
        }
    }
}

I made a Test Case TC3:

import static com.kazurayam.ks.WaitHelpers.waitForDomModificationsToCease

import org.openqa.selenium.WebDriver

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.driver.DriverFactory
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI

import internal.GlobalVariable

/**
 * TC3: automate signing-in to Microsoft Azure DevOps with MutationObserver
 */

TestObject makeTestObject(String id, String xpathExpression) {
    TestObject tObj = new TestObject(id)
    tObj.addProperty("xpath", ConditionType.EQUALS, xpathExpression)
    return tObj
}

String url = "https://dev.azure.com/${GlobalVariable.ACCOUNT}"
WebUI.openBrowser(url)
WebUI.setViewPortSize(800, 800)
WebDriver driver = DriverFactory.getWebDriver()

// to make sure the page is loaded
waitForDomModificationsToCease(driver)

// type the EMAIL as my DevOps account id
TestObject loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
WebUI.sendKeys(loginfmt, GlobalVariable.EMAIL)
waitForDomModificationsToCease(driver)   // wait for the DOM to be static

// click the Next button
TestObject nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
WebUI.click(nextButton)
waitForDomModificationsToCease(driver)

// type the password of my DevOps account
TestObject passwd = makeTestObject("Passwd", "//input[@name='passwd']")
WebUI.sendKeys(passwd, GlobalVariable.PASSWD)
waitForDomModificationsToCease(driver)

// click the Sing-in button
TestObject signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
WebUI.click(signinButton)
waitForDomModificationsToCease(driver)

// click the Yes button
TestObject yesButton = makeTestObject("Yes", "//input[@id='idSIButton9']")
WebUI.click(yesButton)
waitForDomModificationsToCease(driver)

WebUI.delay(3)
WebUI.closeBrowser()

When I run the TC3, usually it passes. I encounter no “stale element reference”. That is a good achievement.

However TC3 often fails due to NullPointerException:

2023-10-14 09:43:10.731 INFO  c.k.katalon.core.main.TestCaseExecutor   - --------------------
2023-10-14 09:43:10.737 INFO  c.k.katalon.core.main.TestCaseExecutor   - START Test Cases/TC3
2023-10-14 09:43:11.424 DEBUG testcase.TC3                             - 1: url = https://dev.azure.com/$GlobalVariable.ACCOUNT
2023-10-14 09:43:11.708 DEBUG testcase.TC3                             - 2: openBrowser(url)
2023-10-14 09:43:12.030 INFO  c.k.k.core.webui.driver.DriverFactory    - Starting 'Chrome' driver
10 14, 2023 9:43:12 午前 org.openqa.selenium.remote.DesiredCapabilities chrome
情報: Using `new ChromeOptions()` is preferred to `DesiredCapabilities.chrome()`
2023-10-14 09:43:12.079 INFO  c.k.k.core.webui.driver.DriverFactory    - Action delay is set to 0 milliseconds
Starting ChromeDriver 118.0.5993.70 (e52f33f30b91b4ddfad649acddc39ab570473b86-refs/branch-heads/5993@{#1216}) on port 22533
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.
10 14, 2023 9:43:15 午前 org.openqa.selenium.remote.ProtocolHandshake createSession
情報: Detected dialect: W3C
2023-10-14 09:43:15.211 INFO  c.k.k.core.webui.driver.DriverFactory    - sessionId = 0955cf3c07d3c6b9a7de9fef7cd7decf
2023-10-14 09:43:15.258 INFO  c.k.k.core.webui.driver.DriverFactory    - browser = Chrome 118.0.0.0
2023-10-14 09:43:15.270 INFO  c.k.k.core.webui.driver.DriverFactory    - platform = Mac OS X
2023-10-14 09:43:15.304 INFO  c.k.k.core.webui.driver.DriverFactory    - seleniumVersion = 3.141.59
2023-10-14 09:43:15.325 INFO  c.k.k.core.webui.driver.DriverFactory    - proxyInformation = ProxyInformation { proxyOption=NO_PROXY, proxyServerType=HTTP, username=, password=********, proxyServerAddress=, proxyServerPort=0, executionList="", isApplyToDesiredCapabilities=true }
2023-10-14 09:43:17.952 DEBUG testcase.TC3                             - 3: setViewPortSize(800, 800)
2023-10-14 09:43:18.171 DEBUG testcase.TC3                             - 4: driver = getWebDriver()
2023-10-14 09:43:18.237 DEBUG testcase.TC3                             - 5: WaitHelpers.waitForDomModificationsToCease(driver)
>>> modifiedTime=0.0
<<< jsx returned window.domModifiedTime=1697244198943
>>> modifiedTime=1.697244198943E12
<<< jsx returned window.domModifiedTime=1697244199270
>>> modifiedTime=1.69724419927E12
<<< jsx returned window.domModifiedTime=1697244199662
>>> modifiedTime=1.697244199662E12
<<< jsx returned window.domModifiedTime=1697244199786
2023-10-14 09:43:20.395 DEBUG testcase.TC3                             - 6: loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
2023-10-14 09:43:20.397 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-14 09:43:20.405 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-14 09:43:20.414 DEBUG testcase.TC3                             - 3: return tObj
2023-10-14 09:43:20.417 DEBUG testcase.TC3                             - 7: sendKeys(loginfmt, EMAIL)
2023-10-14 09:43:21.014 DEBUG testcase.TC3                             - 8: WaitHelpers.waitForDomModificationsToCease(driver)
>>> modifiedTime=0.0
<<< jsx returned window.domModifiedTime=1697244201031
2023-10-14 09:43:21.375 DEBUG testcase.TC3                             - 9: nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
2023-10-14 09:43:21.377 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-14 09:43:21.378 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-14 09:43:21.379 DEBUG testcase.TC3                             - 3: return tObj
2023-10-14 09:43:21.382 DEBUG testcase.TC3                             - 10: click(nextButton)
2023-10-14 09:43:21.765 DEBUG testcase.TC3                             - 11: WaitHelpers.waitForDomModificationsToCease(driver)
>>> modifiedTime=0.0
<<< jsx returned window.domModifiedTime=1697244201780
>>> modifiedTime=1.69724420178E12
<<< jsx returned window.domModifiedTime=null
2023-10-14 09:43:23.247 ERROR c.k.katalon.core.main.TestCaseExecutor   - ❌ Test Cases/TC3 FAILED.
Reason:
java.lang.NullPointerException
	at com.kazurayam.ks.WaitHelpers.waitForDomModificationsToCease(WaitHelpers.groovy:37)
	at com.kazurayam.ks.WaitHelpers$waitForDomModificationsToCease.callStatic(Unknown Source)
	at TC3.run(TC3:38)
	at com.kms.katalon.core.main.ScriptEngine.run(ScriptEngine.java:194)
	at com.kms.katalon.core.main.ScriptEngine.runScriptAsRawText(ScriptEngine.java:119)
	at com.kms.katalon.core.main.TestCaseExecutor.runScript(TestCaseExecutor.java:448)
	at com.kms.katalon.core.main.TestCaseExecutor.doExecute(TestCaseExecutor.java:439)
	at com.kms.katalon.core.main.TestCaseExecutor.processExecutionPhase(TestCaseExecutor.java:418)
	at com.kms.katalon.core.main.TestCaseExecutor.accessMainPhase(TestCaseExecutor.java:410)
	at com.kms.katalon.core.main.TestCaseExecutor.execute(TestCaseExecutor.java:285)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:144)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:135)
	at com.kms.katalon.core.main.TestCaseMain$runTestCase$0.call(Unknown Source)
	at TempTestCase1697244187408.run(TempTestCase1697244187408.groovy:25)

2023-10-14 09:43:23.309 ERROR c.k.katalon.core.main.TestCaseExecutor   - ❌ Test Cases/TC3 FAILED.
Reason:
java.lang.NullPointerException
	at com.kazurayam.ks.WaitHelpers.waitForDomModificationsToCease(WaitHelpers.groovy:37)
	at com.kazurayam.ks.WaitHelpers$waitForDomModificationsToCease.callStatic(Unknown Source)
	at TC3.run(TC3:38)
	at com.kms.katalon.core.main.ScriptEngine.run(ScriptEngine.java:194)
	at com.kms.katalon.core.main.ScriptEngine.runScriptAsRawText(ScriptEngine.java:119)
	at com.kms.katalon.core.main.TestCaseExecutor.runScript(TestCaseExecutor.java:448)
	at com.kms.katalon.core.main.TestCaseExecutor.doExecute(TestCaseExecutor.java:439)
	at com.kms.katalon.core.main.TestCaseExecutor.processExecutionPhase(TestCaseExecutor.java:418)
	at com.kms.katalon.core.main.TestCaseExecutor.accessMainPhase(TestCaseExecutor.java:410)
	at com.kms.katalon.core.main.TestCaseExecutor.execute(TestCaseExecutor.java:285)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:144)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:135)
	at com.kms.katalon.core.main.TestCaseMain$runTestCase$0.call(Unknown Source)
	at TempTestCase1697244187408.run(TempTestCase1697244187408.groovy:25)

2023-10-14 09:43:23.341 INFO  c.k.katalon.core.main.TestCaseExecutor   - END Test Cases/TC3

I could not find out why I got NPE, I coud not fix the bug. Finally, I have abandoned this experiment.

I have improved the com.kazurayam.ks.WaitHelpers class as follows :octopus: :

package com.kazurayam.ks

import org.openqa.selenium.JavascriptExecutor
import org.openqa.selenium.WebDriver

public class WaitHelpers {

    /**
     * wait for the DOM to stop changing.
     * Uses a MutationObserver (JavaScript) to be notified of DOM modifications.
     * "Changing" can include addition/removal/modification of any elements in the DOM
     */
    public static void waitForDomModificationsToCease(WebDriver driver) {
        JavascriptExecutor jsx = (JavascriptExecutor)driver;
        jsx.executeScript("""
if (typeof observer === 'undefined') {
    window.domModifiedTime = Date.now();
    let targetNode = document.querySelector('body');
    let config = {
        attributes: true,
        childList: true,
        subtree: true
    };
    let callback = () => {
        window.domModifiedTime = Date.now();
    };
    let observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
}
""");
        double modifiedTime = 0;
        double start = System.currentTimeMillis();
        double jsxResult = 0;
        while ( {
            jsxResult = (Double)jsx.executeScript("return window.domModifiedTime");
            return modifiedTime != jsxResult && System.currentTimeMillis() - start < 30000;
            }() ) {
            // println ">>> modifiedTime=" + modifiedTime;
            // println "<<< jsxResult   =" + jsxResult;
            modifiedTime = jsxResult;
            Thread.sleep(300);
        }
    }
}

With this version of waitForDomModificationToCease() method, the Test Case TC3 runs stable and passes. It does not throw any NullPointerException. I think I could fix the aforementioned bug.


What’s the difference of waitForDomModificationToCease() method of the previous version and the revised version?

  • previous version
        double modifiedTime = 0;
        double start = System.currentTimeMillis();
        while (modifiedTime != (Double)jsx.executeScript("return window.domModifiedTime") && System.currentTimeMillis() - start < 30000) {
            println ">>> modifiedTime=" + modifiedTime;
            println "<<< jsx returned window.domModifiedTime=" + jsx.executeScript("return window.domModifiedTime");
            modifiedTime = (Double)jsx.executeScript("return window.domModifiedTime");
            Thread.sleep(300);
        }
  • revised version
        double modifiedTime = 0;
        double start = System.currentTimeMillis();
        double jsxResult = 0;
        while ( {
            jsxResult = (Double)jsx.executeScript("return window.domModifiedTime");
            return modifiedTime != jsxResult && System.currentTimeMillis() - start < 30000;
        }() ) {
            // println ">>> modifiedTime=" + modifiedTime;
            // println "<<< jsxResult   =" + jsxResult;
            modifiedTime = jsxResult;
            Thread.sleep(300);
        }

Both tries to do the same processing.

The previous version calls jsx.executeScript("return window.domModifiedTime") repeatedly (3 times per an iteration).

The revised version calls jsx.executeScript("return window.domModifiedTime") only once per an iteration.

Does the number of calls to jsx.executeScript("return window.domModifiedTime") matter? Why?

Yes, it matters much. A single call to jsx.executeScript("...") involves a series of conversations amongst the Test script <==> WebDriver <==> Browser over the TCP network stack. A spike traffic could fail. I should not call jsx.executeScript(...) repeatedly in a short time period.

So, I have changed the code of waitForDomModificationsToCease() method so that it calls jsx.executeScript("...") only once per iteration of while loop.


Now the Test Case TC3 runs quite stable. It throws no “Stale element reference” Exception. The waitForDomModificationToCease() resolves the problem of the original post of this topic.

The waitForDomModificationsToCease method waits for the DOM of the target HTML to become static so that the caller test script can keep in sync with JavaScript in the target HTML. Katalon’s Smart Wait is possibly designed similar to the waitForDomModificationsToCease(). Now I feel comfortable with the Smart Wait. I would use the Smart Wait when necessary.

The com.kazurayam.ks.WaitHelpers#waitForDomModificationsToCease() comes from the idea of @Brandon_Hein. I would express my special thanks to him.

1 Like

Excellent research, as always @kazurayam. I’m going to tweak my version based on some of your findings and hopefully see some improvement in my projects. Thank you!

A comment about the type returned by javascriptExecutor.executeJavascript(String).

The Brandon_Heins’ original code declares Long

 while (modifiedTime != (long) javascriptExecutor.executeScript("return window.domModifiedTime") && System.currentTimeMillis() - start < 30000) {

My code declares Double

        while ( {
            jsxResult = (Double)jsx.executeScript("return window.domModifiedTime");

Why? — I got failures with Long, so I changed it to Double.

Long or Double, which is correct?

Guru99 writes that any number value of JavaScript will be returned as a Long type to the caller in Java/Groovy. But I believe it is a wrong understanding.

The Javadoc of JavascriptExecutor#executeScript() writes:

  • For a decimal, a Double is returned
  • For a non-decimal number, a Long is returned

So the caller in Java/Groovy must be careful which type (Long or Double) to use; Long and Double – both are possible. The returned type varies case by case.

It’s been a while since I looked, but I thought the domModifiedTime came back as non-decimal for me, hence the cast to long.

Confirmed:

Possible that it’s converted to a double based on your specific setup, as you said.

1 Like

You are right.

I wrote a Test Case in Katalon Studio:

import org.openqa.selenium.JavascriptExecutor
import org.openqa.selenium.WebDriver

import com.kms.katalon.core.webui.driver.DriverFactory
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI

import internal.GlobalVariable

String url = "https://dev.azure.com/${GlobalVariable.ACCOUNT}"
WebUI.openBrowser(url)
WebUI.setViewPortSize(800, 800)
WebDriver driver = DriverFactory.getWebDriver()

JavascriptExecutor jsx = (JavascriptExecutor)driver
Object jsxResult = jsx.executeScript("return Date.now()")
println jsxResult.getClass().getName()

When I ran it, I saw:

2023-10-15 07:45:48.749 INFO  c.k.k.core.webui.driver.DriverFactory    - seleniumVersion = 3.141.59
2023-10-15 07:45:48.764 INFO  c.k.k.core.webui.driver.DriverFactory    - proxyInformation = ProxyInformation { proxyOption=NO_PROXY, proxyServerType=HTTP, username=, password=********, proxyServerAddress=, proxyServerPort=0, executionList="", isApplyToDesiredCapabilities=true }
2023-10-15 07:45:51.541 DEBUG testcase.LongOrDouble                    - 3: setViewPortSize(800, 800)
2023-10-15 07:45:51.749 DEBUG testcase.LongOrDouble                    - 4: driver = getWebDriver()
2023-10-15 07:45:51.777 DEBUG testcase.LongOrDouble                    - 5: jsx = driver
2023-10-15 07:45:51.814 DEBUG testcase.LongOrDouble                    - 6: jsxResult = jsx.executeScript("return Date.now()")
2023-10-15 07:45:52.670 DEBUG testcase.LongOrDouble                    - 7: println(getClass().getName())
java.lang.Long
2023-10-15 07:45:52.716 INFO  c.k.katalon.core.main.TestCaseExecutor   - END Test Cases/tests/LongOrDouble

I confirmed JavascriptExecutor#executeScript("return Date.now()") returns java.lang.Long. I would remedy my code.

@Brandon_Hein

I have improved the com.kazurayam.ks.WaitHelper#waitForDomModificationsToCease().

The new code is as follows:

package com.kazurayam.ks

import org.openqa.selenium.JavascriptExecutor
import org.openqa.selenium.WebDriver
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WaitHelpers {

    private static final Logger logger = LoggerFactory.getLogger(WaitHelpers.class)

    /**
     * wait for the DOM to stop changing.
     * Uses a MutationObserver (JavaScript) to be notified of DOM modifications.
     * "Changing" can include addition/removal/modification of any elements in the DOM
     */
    public static void waitForDomModificationsToCease(WebDriver driver) {
        Objects.requireNonNull(driver);
        logger.info("current URL : " + driver.getCurrentUrl());
        JavascriptExecutor jsx = (JavascriptExecutor)driver;
        jsx.executeScript("""
if (typeof observer === 'undefined') {
    window.domModifiedTime = Date.now();
    let targetNode = document.querySelector('body');
    let config = {
        attributes: true,
        childList: true,
        subtree: true
    };
    let callback = () => {
        window.domModifiedTime = Date.now();
    };
    let observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
}
""");
        long modifiedTime = 0;
        long start = System.currentTimeMillis();
        for (;;) {
            Thread.sleep(1000);
            Object rs = jsx.executeScript("return window.domModifiedTime;");
            if (rs != null) {
                long jsxResult = (long)rs;
                logger.debug("<<< modifiedTime ${modifiedTime}");
                logger.debug(">>> jsxResult ${jsxResult}");
                if (modifiedTime == jsxResult) {
                    break;
                };
                modifiedTime = jsxResult;
            } else {
                logger.debug("jsx.executeScript(\"return window.domModifiedTime;\") returned null");
            }
            if (System.currentTimeMillis() - start > 10000) {
                break;
            }
        }
    }
}

In the previous version, I used Groovy Closure:

        double jsxResult = 0;
        while ( {
            jsxResult = (Double)jsx.executeScript("return window.domModifiedTime");
            return modifiedTime != jsxResult && System.currentTimeMillis() - start < 30000;
            }() ) {
            modifiedTime = jsxResult;
            Thread.sleep(300);
        }

It smelled a bit. So I simplified it with the barebone for (;;) { ... } with a conditional break.

Also I inserted several logging statements to see how the code works.

I have got a Test Case TC3 like this:

import static com.kazurayam.ks.WaitHelpers.waitForDomModificationsToCease

import org.openqa.selenium.WebDriver

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.driver.DriverFactory
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI

import internal.GlobalVariable

/**
 * TC3: automate sign-in to Microsoft Azure DevOps with MutationObserver
 */

TestObject makeTestObject(String id, String xpathExpression) {
    TestObject tObj = new TestObject(id)
    tObj.addProperty("xpath", ConditionType.EQUALS, xpathExpression)
    return tObj
}

String url = "https://dev.azure.com/${GlobalVariable.ACCOUNT}"
WebUI.openBrowser(url)
WebUI.setViewPortSize(800, 800)
WebDriver driver = DriverFactory.getWebDriver()

// type the EMAIL as my DevOps account id
waitForDomModificationsToCease(driver)   // wait for the DOM to be static
TestObject loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
WebUI.sendKeys(loginfmt, GlobalVariable.EMAIL)

// click the Next button
waitForDomModificationsToCease(driver)
TestObject nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
WebUI.click(nextButton)

// type the password of my DevOps account
waitForDomModificationsToCease(driver)
TestObject passwd = makeTestObject("Passwd", "//input[@name='passwd']")
WebUI.sendKeys(passwd, GlobalVariable.PASSWD)

// click the Sing-in button
waitForDomModificationsToCease(driver)
TestObject signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
WebUI.click(signinButton)

// click the Yes button
waitForDomModificationsToCease(driver)
TestObject yesButton = makeTestObject("Yes", "//input[@id='idSIButton9']")
WebUI.click(yesButton)

WebUI.delay(1)
WebUI.closeBrowser()

When I execute it, I got the following output in the console:

2023-10-17 17:30:14.137 INFO  c.k.katalon.core.main.TestCaseExecutor   - --------------------
2023-10-17 17:30:14.139 INFO  c.k.katalon.core.main.TestCaseExecutor   - START Test Cases/TC3
2023-10-17 17:30:14.807 DEBUG testcase.TC3                             - 1: url = https://dev.azure.com/$GlobalVariable.ACCOUNT
2023-10-17 17:30:14.999 DEBUG testcase.TC3                             - 2: openBrowser(url)
2023-10-17 17:30:15.351 INFO  c.k.k.core.webui.driver.DriverFactory    - Starting 'Chrome' driver
10 17, 2023 5:30:15 午後 org.openqa.selenium.remote.DesiredCapabilities chrome
情報: Using `new ChromeOptions()` is preferred to `DesiredCapabilities.chrome()`
2023-10-17 17:30:15.398 INFO  c.k.k.core.webui.driver.DriverFactory    - Action delay is set to 0 milliseconds
Starting ChromeDriver 118.0.5993.70 (e52f33f30b91b4ddfad649acddc39ab570473b86-refs/branch-heads/5993@{#1216}) on port 6329
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.
10 17, 2023 5:30:18 午後 org.openqa.selenium.remote.ProtocolHandshake createSession
情報: Detected dialect: W3C
2023-10-17 17:30:18.356 INFO  c.k.k.core.webui.driver.DriverFactory    - sessionId = b53e28e287bdafb23ef95e0e3094ed42
2023-10-17 17:30:18.496 INFO  c.k.k.core.webui.driver.DriverFactory    - browser = Chrome 118.0.0.0
2023-10-17 17:30:18.497 INFO  c.k.k.core.webui.driver.DriverFactory    - platform = Mac OS X
2023-10-17 17:30:18.497 INFO  c.k.k.core.webui.driver.DriverFactory    - seleniumVersion = 3.141.59
2023-10-17 17:30:18.499 INFO  c.k.k.core.webui.driver.DriverFactory    - proxyInformation = ProxyInformation { proxyOption=NO_PROXY, proxyServerType=HTTP, username=, password=********, proxyServerAddress=, proxyServerPort=0, executionList="", isApplyToDesiredCapabilities=true }
2023-10-17 17:30:22.336 DEBUG testcase.TC3                             - 3: setViewPortSize(800, 800)
2023-10-17 17:30:22.575 DEBUG testcase.TC3                             - 4: driver = getWebDriver()
2023-10-17 17:30:22.635 DEBUG testcase.TC3                             - 5: WaitHelpers.waitForDomModificationsToCease(driver)
2023-10-17 17:30:23.495 INFO  com.kazurayam.ks.WaitHelpers             - current URL : https://login.microsoftonline.com/common/oauth2/authorize?client_id=499b84ac-1321-427f-aa17-267ca6975798&site_id=501454&response_mode=form_post&response_type=code+id_token&redirect_uri=https%3A%2F%2Fspsprodcus5.vssps.visualstudio.com%2F_signedin&nonce=7d00d652-2420-452b-834e-ffac1c9375e7&state=realm%3Ddev.azure.com%26reply_to%3Dhttps%253A%252F%252Fdev.azure.com%252Fkazuakiurayama%252F%26ht%3D2%26mkt%3Dja-JP%26hid%3Dff95d235-a8d2-4797-ab65-36cdd3db8d01%26nonce%3D7d00d652-2420-452b-834e-ffac1c9375e7&resource=https%3A%2F%2Fmanagement.core.windows.net%2F&cid=7d00d652-2420-452b-834e-ffac1c9375e7&wsucxt=1&githubsi=true&msaoauth2=true&mkt=ja-JP
2023-10-17 17:30:24.966 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 0
2023-10-17 17:30:24.966 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531424245
2023-10-17 17:30:25.995 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 1697531424245
2023-10-17 17:30:25.995 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531424245
2023-10-17 17:30:25.996 DEBUG testcase.TC3                             - 6: loginfmt = makeTestObject("loginfmt", "//input[@name='loginfmt']")
2023-10-17 17:30:25.998 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-17 17:30:26.015 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-17 17:30:26.034 DEBUG testcase.TC3                             - 3: return tObj
2023-10-17 17:30:26.036 DEBUG testcase.TC3                             - 7: sendKeys(loginfmt, EMAIL)
2023-10-17 17:30:26.830 DEBUG testcase.TC3                             - 8: WaitHelpers.waitForDomModificationsToCease(driver)
2023-10-17 17:30:26.844 INFO  com.kazurayam.ks.WaitHelpers             - current URL : https://login.microsoftonline.com/common/oauth2/authorize?client_id=499b84ac-1321-427f-aa17-267ca6975798&site_id=501454&response_mode=form_post&response_type=code+id_token&redirect_uri=https%3A%2F%2Fspsprodcus5.vssps.visualstudio.com%2F_signedin&nonce=7d00d652-2420-452b-834e-ffac1c9375e7&state=realm%3Ddev.azure.com%26reply_to%3Dhttps%253A%252F%252Fdev.azure.com%252Fkazuakiurayama%252F%26ht%3D2%26mkt%3Dja-JP%26hid%3Dff95d235-a8d2-4797-ab65-36cdd3db8d01%26nonce%3D7d00d652-2420-452b-834e-ffac1c9375e7&resource=https%3A%2F%2Fmanagement.core.windows.net%2F&cid=7d00d652-2420-452b-834e-ffac1c9375e7&wsucxt=1&githubsi=true&msaoauth2=true&mkt=ja-JP
2023-10-17 17:30:27.877 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 0
2023-10-17 17:30:27.895 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531426855
2023-10-17 17:30:28.935 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 1697531426855
2023-10-17 17:30:28.936 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531426855
2023-10-17 17:30:28.936 DEBUG testcase.TC3                             - 9: nextButton = makeTestObject("Next", "//input[@id='idSIButton9']")
2023-10-17 17:30:28.938 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-17 17:30:28.940 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-17 17:30:28.943 DEBUG testcase.TC3                             - 3: return tObj
2023-10-17 17:30:28.947 DEBUG testcase.TC3                             - 10: click(nextButton)
2023-10-17 17:30:29.362 DEBUG testcase.TC3                             - 11: WaitHelpers.waitForDomModificationsToCease(driver)
2023-10-17 17:30:29.407 INFO  com.kazurayam.ks.WaitHelpers             - current URL : https://login.microsoftonline.com/common/oauth2/authorize?client_id=499b84ac-1321-427f-aa17-267ca6975798&site_id=501454&response_mode=form_post&response_type=code+id_token&redirect_uri=https%3A%2F%2Fspsprodcus5.vssps.visualstudio.com%2F_signedin&nonce=7d00d652-2420-452b-834e-ffac1c9375e7&state=realm%3Ddev.azure.com%26reply_to%3Dhttps%253A%252F%252Fdev.azure.com%252Fkazuakiurayama%252F%26ht%3D2%26mkt%3Dja-JP%26hid%3Dff95d235-a8d2-4797-ab65-36cdd3db8d01%26nonce%3D7d00d652-2420-452b-834e-ffac1c9375e7&resource=https%3A%2F%2Fmanagement.core.windows.net%2F&cid=7d00d652-2420-452b-834e-ffac1c9375e7&wsucxt=1&githubsi=true&msaoauth2=true&mkt=ja-JP
2023-10-17 17:30:31.398 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:32.411 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:33.427 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:34.442 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:35.458 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:36.475 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:37.492 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:38.510 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:39.561 DEBUG com.kazurayam.ks.WaitHelpers             - jsx.executeScript("return window.domModifiedTime;") returned null
2023-10-17 17:30:39.563 DEBUG testcase.TC3                             - 12: passwd = makeTestObject("Passwd", "//input[@name='passwd']")
2023-10-17 17:30:39.565 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-17 17:30:39.568 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-17 17:30:39.569 DEBUG testcase.TC3                             - 3: return tObj
2023-10-17 17:30:39.571 DEBUG testcase.TC3                             - 13: sendKeys(passwd, PASSWD)
2023-10-17 17:30:39.962 DEBUG testcase.TC3                             - 14: WaitHelpers.waitForDomModificationsToCease(driver)
2023-10-17 17:30:39.983 INFO  com.kazurayam.ks.WaitHelpers             - current URL : https://login.live.com/oauth20_authorize.srf?scope=openid+profile+email+offline_access&response_type=code&client_id=51483342-085c-4d86-bf88-cf50c7252078&response_mode=form_post&redirect_uri=https%3a%2f%2flogin.microsoftonline.com%2fcommon%2ffederation%2foauth2msa&state=rQQIARAAnZE_aBNRHMfvJU3alGqDk4NDhzSD8SV37-7dywVuaEpbrba1tSXBJbzcuySX3D_v3SWm0MnFSQoVh46OQbCIgzi5uDh1chCclYLgP7CbvQYc3ET48YPvj-_w-X2_V5NSUarkFE1rlhVqQElGElQQaUFKJQKRSgyqagQTrbyaxqKkYCW4NJ3dei_szeUKSw_Wfz-7chB9OgSZhm31zaLhOSOgdMLQ55VSifvcDzxmRBwX-zxWxb7FI2rzMGKWd24uNbjVdk1mua8AOAbgMwCHiVSXwtXbo0SOMFFkKkYQKUiECkZNWJYVE7Za1JAMTSbYJB8SsxsLUdhB58sLrF3zeyLT8gKn4Xs8PEw-B4FJbUdnZr9Id6NgzJgPTN8eNkJPH6POywvzaDmev0yx7sWC9qwooEPq0PiQ74Q6yju9UB8z5jsW01stDTMkY0jLDEGFaATSpoqhrBqMyaxZZqKUdz3XMPV_eWiUzP3Jz6EubZuO6YYxTww1sFzmDXjRNcPSi2Q6ZnQ893gCnEzMiKnK1NR0dvby5JxwOgGepuKWftUf3ZOT5eXHfP2gsseFd6nSfanftUukoKI7W3SzUFhbr2FW361W_Y5PQms4CNRub9BZuTFs66Qi7afBfjr9NQ0eTgqvM__V69EMeHNBOL349snPby8__vhy_WT2WlDbXKxaG2vbuFrfCWl9Z-2mvXzX8hW_rt7a5vIS3l7pDWuLNXFJP8oKZw2&estsfed=1&uaid=7d00d6522420452b834effac1c9375e7&cobrandid=31ffa2bb-717b-40bc-831f-f7f1a66612db&fci=499b84ac-1321-427f-aa17-267ca6975798&wsucxt=1&mkt=ja-JP&username=kazuaki.urayama%40gmail.com&login_hint=kazuaki.urayama%40gmail.com
2023-10-17 17:30:41.017 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 0
2023-10-17 17:30:41.017 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531439996
2023-10-17 17:30:42.032 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 1697531439996
2023-10-17 17:30:42.032 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531439996
2023-10-17 17:30:42.033 DEBUG testcase.TC3                             - 15: signinButton = makeTestObject("SignIn", "//input[@id='idSIButton9']")
2023-10-17 17:30:42.034 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-17 17:30:42.036 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-17 17:30:42.038 DEBUG testcase.TC3                             - 3: return tObj
2023-10-17 17:30:42.040 DEBUG testcase.TC3                             - 16: click(signinButton)
2023-10-17 17:30:43.245 DEBUG testcase.TC3                             - 17: WaitHelpers.waitForDomModificationsToCease(driver)
2023-10-17 17:30:43.331 INFO  com.kazurayam.ks.WaitHelpers             - current URL : https://login.live.com/ppsecure/post.srf?mkt=ja-JP&username=kazuaki.urayama%40gmail.com&client_id=51483342-085c-4d86-bf88-cf50c7252078&cobrandid=31ffa2bb-717b-40bc-831f-f7f1a66612db&contextid=1AAD68E788B1A0FF&opid=543D491D1BB536B4&bk=1697531430&uaid=7d00d6522420452b834effac1c9375e7&pid=15216
2023-10-17 17:30:44.374 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 0
2023-10-17 17:30:44.374 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531443889
2023-10-17 17:30:45.387 DEBUG com.kazurayam.ks.WaitHelpers             - <<< modifiedTime 1697531443889
2023-10-17 17:30:45.403 DEBUG com.kazurayam.ks.WaitHelpers             - >>> jsxResult 1697531443889
2023-10-17 17:30:45.403 DEBUG testcase.TC3                             - 18: yesButton = makeTestObject("Yes", "//input[@id='idSIButton9']")
2023-10-17 17:30:45.405 DEBUG testcase.TC3                             - 1: tObj = new com.kms.katalon.core.testobject.TestObject(id)
2023-10-17 17:30:45.406 DEBUG testcase.TC3                             - 2: tObj.addProperty("xpath", EQUALS, xpathExpression)
2023-10-17 17:30:45.407 DEBUG testcase.TC3                             - 3: return tObj
2023-10-17 17:30:45.409 DEBUG testcase.TC3                             - 19: click(yesButton)
2023-10-17 17:30:51.688 DEBUG testcase.TC3                             - 20: delay(1)
2023-10-17 17:30:52.768 DEBUG testcase.TC3                             - 21: closeBrowser()
2023-10-17 17:30:53.010 INFO  c.k.katalon.core.main.TestCaseExecutor   - END Test Cases/TC3

Here I encountered some strange messages:

jsx.executeScript("return window.domModifiedTime;") returned null

This message proves that my code (com.kazurayam.ks.WaitHelper) has something wrong. I tried to fix this problem, but I couldn’t. I gave up.

I would ask you (anyone who is interested in this topic) NOT to follow my “general wait” implementation. You would waste your time. I would rather recommend you to use Katlaon’s “Smart Wait” as it works fine.

1 Like