Skip to content

Commit 592b3d1

Browse files
shossain786claude
andauthored
spike(locator): stale-proof click — re-resolve on every poll (#67)
Locator.click() resolved the element once, then waited on that reference. If the page re-rendered it the wait either timed out (stale during the clickable check) or StaleElementReferenceException escaped (stale between check and click). click() now resolves inside the wait, ignores StaleElementReferenceException, and re-resolves each poll. Spike scope: click() only. Other terminal actions unchanged. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 27ed654 commit 592b3d1

2 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎src/main/java/com/seleniumboot/locator/Locator.java‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.openqa.selenium.By;
77
import org.openqa.selenium.JavascriptExecutor;
88
import org.openqa.selenium.WebDriver;
9+
import org.openqa.selenium.StaleElementReferenceException;
910
import org.openqa.selenium.WebElement;
1011
import org.openqa.selenium.interactions.Actions;
1112
import org.openqa.selenium.support.ui.ExpectedConditions;
@@ -221,9 +222,21 @@ public By toBy() {
221222
// Terminal actions — all auto-wait
222223
// ------------------------------------------------------------------
223224

224-
/** Waits for the element to be clickable, then clicks it. */
225+
/**
226+
* Waits for the element to be clickable, then clicks it. The element is re-resolved on every
227+
* poll, so a page that re-renders it between lookup and click does not surface as a
228+
* {@link StaleElementReferenceException}.
229+
*/
225230
public void click() {
226-
waitForClickable(resolve()).click();
231+
int timeout = SeleniumBootContext.getConfig().getTimeouts().getExplicit();
232+
new WebDriverWait(driver(), Duration.ofSeconds(timeout))
233+
.ignoring(StaleElementReferenceException.class)
234+
.until(d -> {
235+
WebElement el = ExpectedConditions.elementToBeClickable(resolve()).apply(d);
236+
if (el == null) return null;
237+
el.click();
238+
return Boolean.TRUE;
239+
});
227240
}
228241

229242
/** Waits for the element to be visible, clears it, then types the given text. */
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.seleniumboot.unit;
2+
3+
import com.seleniumboot.config.SeleniumBootConfig;
4+
import com.seleniumboot.driver.DriverManager;
5+
import com.seleniumboot.internal.SeleniumBootContext;
6+
import com.seleniumboot.locator.Locator;
7+
import org.mockito.MockedStatic;
8+
import org.openqa.selenium.By;
9+
import org.openqa.selenium.StaleElementReferenceException;
10+
import org.openqa.selenium.WebDriver;
11+
import org.openqa.selenium.WebElement;
12+
import org.testng.annotations.AfterMethod;
13+
import org.testng.annotations.BeforeMethod;
14+
import org.testng.annotations.Test;
15+
16+
import java.util.List;
17+
18+
import static org.mockito.Mockito.*;
19+
import static org.testng.Assert.assertEquals;
20+
21+
/**
22+
* {@link Locator} terminal actions must survive the page re-rendering the element between
23+
* resolving it and acting on it — they re-resolve instead of reusing a stale reference.
24+
*/
25+
public class LocatorStaleElementTest {
26+
27+
private WebDriver driver;
28+
private MockedStatic<DriverManager> driverManagerMock;
29+
private MockedStatic<SeleniumBootContext> contextMock;
30+
31+
@BeforeMethod
32+
public void setup() {
33+
driver = mock(WebDriver.class);
34+
driverManagerMock = mockStatic(DriverManager.class);
35+
driverManagerMock.when(DriverManager::getDriver).thenReturn(driver);
36+
37+
SeleniumBootConfig.Timeouts timeouts = new SeleniumBootConfig.Timeouts();
38+
timeouts.setExplicit(2);
39+
SeleniumBootConfig config = new SeleniumBootConfig();
40+
config.setTimeouts(timeouts);
41+
contextMock = mockStatic(SeleniumBootContext.class);
42+
contextMock.when(SeleniumBootContext::getConfig).thenReturn(config);
43+
}
44+
45+
@AfterMethod
46+
public void teardown() {
47+
driverManagerMock.close();
48+
contextMock.close();
49+
}
50+
51+
private static WebElement liveElement() {
52+
WebElement el = mock(WebElement.class);
53+
when(el.isDisplayed()).thenReturn(true);
54+
when(el.isEnabled()).thenReturn(true);
55+
return el;
56+
}
57+
58+
@Test
59+
public void click_elementGoesStaleBetweenCheckAndClick_reResolvesAndClicksFreshElement() {
60+
WebElement stale = liveElement();
61+
doThrow(new StaleElementReferenceException("re-rendered")).when(stale).click();
62+
WebElement fresh = liveElement();
63+
By by = By.id("save");
64+
when(driver.findElements(by)).thenReturn(List.of(stale)).thenReturn(List.of(fresh));
65+
66+
Locator.of(by).click();
67+
68+
verify(fresh, times(1)).click();
69+
}
70+
71+
@Test
72+
public void click_elementIsStaleWhileWaitingForClickable_reResolves() {
73+
WebElement stale = mock(WebElement.class);
74+
when(stale.isDisplayed()).thenThrow(new StaleElementReferenceException("detached"));
75+
WebElement fresh = liveElement();
76+
By by = By.id("save");
77+
when(driver.findElements(by)).thenReturn(List.of(stale)).thenReturn(List.of(fresh));
78+
79+
Locator.of(by).click();
80+
81+
verify(fresh, times(1)).click();
82+
}
83+
84+
@Test
85+
public void click_elementNeverGoesStale_clicksExactlyOnce() {
86+
WebElement el = liveElement();
87+
By by = By.id("save");
88+
when(driver.findElements(by)).thenReturn(List.of(el));
89+
90+
Locator.of(by).click();
91+
92+
verify(el, times(1)).click();
93+
assertEquals(mockingDetails(driver).getInvocations().stream()
94+
.filter(i -> i.getMethod().getName().equals("findElements")).count(), 1L);
95+
}
96+
}

0 commit comments

Comments
 (0)