Stale Element when handling a table

Hello everyone,
I got the “error stale element : element is not attached to the page document” when handling a table.
This is my context :
Here is the table:


I want to search in each column of all rows which have 2018 and click Delete of this rows. When clicking the Delete link, a new page will appear and il’ll continue my test.
I use the following code :

String expectedContract = '2018'

WebDriver driver = DriverFactory.getWebDriver()

WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))

List<WebElement> rows = siteTable.findElements(By.tagName('tr'))

for (int i = 0; i < rows.size(); i++) {
    List<WebElement> cels = rows.get(i).findElements(By.tagName('td'))
	
    for (int j = 0; j < cels.size(); j++) {
        if (cels.get(j).getText().equalsIgnoreCase(expectedContract)) {
			try {
				wait.until(ExpectedConditions.presenceOfElementLocated(cels))
				cels.get(5).findElement(By.tagName('a')).click()
				break
			} catch(StaleElementReferenceException e) {
					driver.navigate().refresh()
					cels.get(5).findElement(By.tagName('a')).click()
			}
        }
    }
}

When executing this code, It found the element that I want to click, It perform the click and pass to another page. But after that, It throws error “error stale element : element is not attached to the page document”. Here is the error in console:

Reason:
org.openqa.selenium.StaleElementReferenceException: stale element reference: element is not attached to the page document
  (Session info: chrome=109.0.5414.75)
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: 'GA4PFL7S3', ip: '10.21.80.45', os.name: 'Windows 10', os.arch: 'amd64', os.version: '10.0', java.version: '1.8.0_282'
Driver info: com.kms.katalon.selenium.driver.CChromeDriver
Capabilities {acceptInsecureCerts: false, browserName: chrome, browserVersion: 109.0.5414.75, chrome: {chromedriverVersion: 109.0.5414.74 (e7c5703604da..., userDataDir: C:\Users\ttmooc\AppData\Loc...}, goog:chromeOptions: {debuggerAddress: localhost:57987}, javascriptEnabled: true, networkConnectionEnabled: false, pageLoadStrategy: normal, platform: WINDOWS, platformName: WINDOWS, 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:virtualAuthenticators: true}
Session ID: 880b29b8431823b15c2440cf26587202
*** Element info: {Using=tag name, value=td}
	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.findElements(RemoteWebElement.java:204)
	at org.openqa.selenium.remote.RemoteWebElement.findElementsByTagName(RemoteWebElement.java:281)
	at org.openqa.selenium.By$ByTagName.findElements(By.java:312)
	at org.openqa.selenium.remote.RemoteWebElement.findElements(RemoteWebElement.java:177)
	at org.openqa.selenium.support.events.EventFiringWebDriver$EventFiringWebElement.lambda$new$0(EventFiringWebDriver.java:404)
	at com.sun.proxy.$Proxy11.findElements(Unknown Source)
	at org.openqa.selenium.support.events.EventFiringWebDriver$EventFiringWebElement.findElements(EventFiringWebDriver.java:504)
	at org.openqa.selenium.WebElement$findElements$0.call(Unknown Source)
	at Handle Table.run(Handle Table:70)
	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:142)
	at com.kms.katalon.core.main.TestCaseMain.runTestCase(TestCaseMain.java:133)
	at com.kms.katalon.core.main.TestCaseMain$runTestCase$0.call(Unknown Source)
	at TempTestCase1673444668268.run(TempTestCase1673444668268.groovy:25)

Can some one help to fix this error please? Thank you

Then you need one more loop which continues until you find no more ‘2018’ to click.

Everytime you reached to a new page, you should start with calling

WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))

You need to fetch a live reference to the table in the new page.

When a StaleElementReferenceException was raised, the variable cels has an obsolete reference to something which not already existent. Therefore this try ... catch ... clause solves nothing.

How can I fetch a live reference to this table?
And I don’t understand why I should have another loop until I find no more ‘2018’. I can’t use 2 for loop to find the element and quit the loop when element found?
I still have to start calling WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]')) when I quit the page that have the table? The new page does’nt have this table I mean

By this:

WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))

Your code will look something like:

String expectedContract = '2018'
WebDriver driver = DriverFactory.getWebDriver()

// loop until you have processed all <a> elements of '2018'
while (hasMoreContractsToClick(driver, expectedContract)) {  
    clickContract(driver, expectedContract)
}

Boolean hasMoreContractsToClick(WebDriver driver, String expectedContract) {
    WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))
    List<WebElement> rows = siteTable.findElements(By.tagName('tr'))
    // you want to check if there is 1 or more <a> tag to click
    return (test if there is 1 or more <a> of '2018')    
}

void clickContract(WebDriver driver, String expectedContract) {
    WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))
    List<WebElement> rows = siteTable.findElements(By.tagName('tr'))
    // you want to click a <a> of '2018', which will move the browser to a new page
}

Then you would want:

void clickContract(....) {
    ....
    // you want to click a <a> of '2018', which will move the browser to a new page
    
    // here you are on a new page; but you want to go back to the page with <table>
    driver.back();
    
    // you want to make sure that your are really back to the page with <table>
     
}

If driver.back does not work for some reason, then you would want to repeat nagivigating to the page again and again until you process all elements to click:

Boolean hasMoreContractsToClick(WebDriver driver, String expectedContract) {
    driver.to( /* the URL of the page with <table>*/)
    WebElement siteTable = driver.findELement(....
    ....
}

No, I don’t want to back to the page with I want to continue another action.

String expectedContract = '2018'
WebDriver driver = DriverFactory.getWebDriver()

// loop until you have processed all <a> elements of '2018'
while (hasMoreContractsToClick(driver, expectedContract)) {
	clickContract(driver, expectedContract)
}

Boolean hasMoreContractsToClick(WebDriver driver, String expected) {
	WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))
	List<WebElement> rows = siteTable.findElements(By.tagName('tr'))
	for (int i = 0; i < rows.size(); i++) {
		List<WebElement> cels = rows.get(i).findElements(By.tagName('td'))
		for (int j = 0; j < cels.size(); j++) {
			if (cels.get(j).getText().equalsIgnoreCase(expected)) {
				println(cels.get(j).getText())
			}
		}
	}
	return true
}

void clickContract(WebDriver driver, String expected) {
	WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))
	List<WebElement> rows = siteTable.findElements(By.tagName('tr'))
	for (int i = 0; i < rows.size(); i++) {
		List<WebElement> cels = rows.get(i).findElements(By.tagName('td'))
		for (int j = 0; j < cels.size(); j++) {
			if (cels.get(j).getText().equalsIgnoreCase(expected)) {
				cels.get(5).findElement(By.tagName('a')).click()
			}
		}
	}
}

Here my correction, but I still have stale element for result. Maybe my hasMoreContractsToClick have a wrong return?

Your hasMoreContractsToClick is always returning true. You need to return false if expectedContract was not found.

So I have only return false when expectedContract not found in my function? Like :

if (!cels.get(j).getText().equalsIgnoreCase(expected)) {
	println(cels.get(j).getText())
	return false
}

You would have to set a variable to false, and only set variable true in the “if” statement, before or after the “println” if expectedContract is found, then return that variable.

	WebElement siteTable = driver.findElement(By.xpath('//table[@class="contractList widgetBodyTable"]'))
	List<WebElement> rows = siteTable.findElements(By.tagName('tr'))
	Boolean found = false
	for (int i = 0; i < rows.size(); i++) {
		List<WebElement> cels = rows.get(i).findElements(By.tagName('td'))
		for (int j = 0; j < cels.size(); j++) {
			if (cels.get(j).getText().equalsIgnoreCase(expected)) {
				println(cels.get(j).getText())
				found = true
			}
		}
	}
	return found
}```

I use this, but I still stale element, but the action is performed :upside_down_face:

When you click delete on any row, does a new page load? or do you stay on the page in question? If new page loads, then as @kazurayam says, you will need to go back to the page in question before trying the action again.

When you delete a row in the table, does it get removed completely, or is it just greyed out? If it still exists in the table but the action to edit or delete is disabled, then you will need to approach it in another way such as remembering which rows have been deleted already and not try and delete them again, or check if the delete button is visible and/or clickable.

I want to Edit. So when I clicked, It will navigate to another page. In this another page, i’ll do something at this page and terminate my TC. So I used break to quit the loop before performing another action in the new page

I don’t want to go back, I meaned

So let me see if I understand you correctly.

  1. You navigate to the page with the table.
  2. You loop over table and find a row with expectedContract.
  3. You click delete on that row.
  4. A new page loads.

If this is the case, then looping will not work unless, after the delete and new page load, you navigate back the page with the table. Put the “navigate back to page with the table” after the “click” in the clickContact method.

In the case where you do not want to go back, that contradicts your original post where you said you want to delete all rows with expectedContract.