Skip to content

Commit ef92099

Browse files
committed
Make cart, checkout, navigation tests reliable in CI
CI against live SauceDemo surfaced deterministic failures in cart/menu flows (login, inventory and sorting already passed). Fixes: - Confirm add/remove by waiting for the product button to toggle Add<->Remove, removing cart-badge read races. - Open the cart via a JS click on the anchor: once the badge is shown it overlaps the link centre, so a native click hit the badge span and navigation never fired. - JS-click burger-menu items (logout, reset, close) to avoid missing the element while the menu is mid-animation; wait on aria-hidden state. - Make BasePage.click scroll into view with a JS-click fallback. - Confirm cart removal by waiting for the row to disappear. - Run the regression suite sequentially for CI stability (framework remains parallel-safe via the ThreadLocal driver).
1 parent 174c81c commit ef92099

5 files changed

Lines changed: 85 additions & 20 deletions

File tree

src/main/java/com/saucedemo/pages/BasePage.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.saucedemo.config.ConfigManager;
44
import org.openqa.selenium.By;
5+
import org.openqa.selenium.ElementClickInterceptedException;
6+
import org.openqa.selenium.JavascriptExecutor;
57
import org.openqa.selenium.WebDriver;
68
import org.openqa.selenium.WebElement;
79
import org.openqa.selenium.support.ui.ExpectedConditions;
@@ -41,8 +43,35 @@ protected WebElement waitForClickable(By locator) {
4143
return wait.until(ExpectedConditions.elementToBeClickable(locator));
4244
}
4345

46+
/**
47+
* Clicks an element after scrolling it into view, falling back to a
48+
* JavaScript click if the native click is intercepted (e.g. by a sticky
49+
* header or an in-flight animation). This keeps interactions reliable in
50+
* headless/CI runs without resorting to sleeps.
51+
*/
4452
protected void click(By locator) {
45-
waitForClickable(locator).click();
53+
WebElement element = waitForClickable(locator);
54+
scrollIntoView(element);
55+
try {
56+
element.click();
57+
} catch (ElementClickInterceptedException e) {
58+
jsClick(element);
59+
}
60+
}
61+
62+
/** Dispatches a click via JavaScript; robust against animated/offset elements. */
63+
protected void jsClick(By locator) {
64+
jsClick(wait.until(ExpectedConditions.presenceOfElementLocated(locator)));
65+
}
66+
67+
private void jsClick(WebElement element) {
68+
scrollIntoView(element);
69+
((JavascriptExecutor) driver).executeScript("arguments[0].click();", element);
70+
}
71+
72+
protected void scrollIntoView(WebElement element) {
73+
((JavascriptExecutor) driver)
74+
.executeScript("arguments[0].scrollIntoView({block:'center'});", element);
4675
}
4776

4877
protected void type(By locator, String text) {

src/main/java/com/saucedemo/pages/CartPage.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.openqa.selenium.By;
55
import org.openqa.selenium.WebDriver;
66
import org.openqa.selenium.WebElement;
7+
import org.openqa.selenium.support.ui.ExpectedConditions;
78

89
import java.util.List;
910

@@ -45,11 +46,11 @@ public boolean containsItem(String productName) {
4546

4647
@Step("Remove product from cart: {productName}")
4748
public CartPage removeItem(String productName) {
48-
By removeButton = By.xpath(
49-
"//div[contains(@class,'cart_item')]"
50-
+ "[.//*[@data-test='inventory-item-name' and normalize-space()=\"" + productName + "\"]]"
51-
+ "//button");
52-
click(removeButton);
49+
String rowXpath = "//div[contains(@class,'cart_item')]"
50+
+ "[.//*[@data-test='inventory-item-name' and normalize-space()=\"" + productName + "\"]]";
51+
click(By.xpath(rowXpath + "//button"));
52+
// Confirm removal: the item's row is gone from the cart.
53+
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.xpath(rowXpath)));
5354
return this;
5455
}
5556

src/main/java/com/saucedemo/pages/HeaderComponent.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ public HeaderComponent(WebDriver driver) {
2929
@Step("Open the navigation menu")
3030
public HeaderComponent openMenu() {
3131
click(BURGER_MENU_BUTTON);
32-
waitForClickable(LOGOUT_LINK);
32+
// Menu is open once the wrapper is no longer aria-hidden.
33+
wait.until(ExpectedConditions.attributeToBe(MENU_WRAP, "aria-hidden", "false"));
34+
wait.until(ExpectedConditions.presenceOfElementLocated(LOGOUT_LINK));
3335
return this;
3436
}
3537

3638
@Step("Close the navigation menu")
3739
public HeaderComponent closeMenu() {
38-
click(CLOSE_MENU_BUTTON);
40+
// JS-click avoids missing the cross button while the menu is animating.
41+
jsClick(CLOSE_MENU_BUTTON);
3942
// The menu wrapper is hidden (aria-hidden=true) once the slide-out finishes.
4043
wait.until(ExpectedConditions.attributeToBe(MENU_WRAP, "aria-hidden", "true"));
4144
return this;
@@ -50,21 +53,28 @@ public boolean isMenuOpen() {
5053
@Step("Log out")
5154
public LoginPage logout() {
5255
openMenu();
53-
click(LOGOUT_LINK);
56+
// JS-click the link to avoid the moving-target problem during the slide.
57+
jsClick(LOGOUT_LINK);
58+
// Logout is complete once the login form is back.
59+
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("login-button")));
5460
return new LoginPage(driver);
5561
}
5662

5763
@Step("Reset application state")
5864
public HeaderComponent resetAppState() {
5965
openMenu();
60-
click(RESET_LINK);
66+
jsClick(RESET_LINK);
6167
closeMenu();
6268
return this;
6369
}
6470

6571
@Step("Open the cart")
6672
public CartPage openCart() {
67-
click(CART_LINK);
73+
// JS-click the anchor itself: once a badge is shown it overlaps the link
74+
// centre, and a native click can land on the badge span instead of the
75+
// anchor, so navigation never fires.
76+
jsClick(CART_LINK);
77+
wait.until(ExpectedConditions.urlContains("cart.html"));
6878
return new CartPage(driver);
6979
}
7080

src/main/java/com/saucedemo/pages/InventoryPage.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,17 @@ public InventoryPage sortBy(SortOption option) {
7979

8080
@Step("Add product to cart: {productName}")
8181
public InventoryPage addProductToCart(String productName) {
82-
actionButtonFor(productName).click();
82+
clickActionButton(productName);
83+
// Confirm the action registered: the button toggles to "Remove".
84+
waitForButtonState(productName, "remove");
8385
return this;
8486
}
8587

8688
@Step("Remove product from cart: {productName}")
8789
public InventoryPage removeProductFromCart(String productName) {
88-
actionButtonFor(productName).click();
90+
clickActionButton(productName);
91+
// Confirm the action registered: the button toggles back to "Add to cart".
92+
waitForButtonState(productName, "add-to-cart");
8993
return this;
9094
}
9195

@@ -94,16 +98,35 @@ public HeaderComponent header() {
9498
}
9599

96100
/**
97-
* Returns the Add/Remove button that belongs to the card for {@code productName}.
98-
* Locating relative to the product card keeps the action robust regardless of
99-
* the current sort order or the button's add/remove state.
101+
* Clicks the Add/Remove button on the card for {@code productName}. Locating
102+
* relative to the product card keeps the action robust regardless of the
103+
* current sort order or the button's add/remove state.
100104
*/
101-
private WebElement actionButtonFor(String productName) {
102-
By itemButton = By.xpath(
105+
private void clickActionButton(String productName) {
106+
click(actionButtonLocator(productName));
107+
}
108+
109+
private By actionButtonLocator(String productName) {
110+
return By.xpath(
103111
"//div[contains(@class,'inventory_item')]"
104112
+ "[.//*[@data-test='inventory-item-name' and normalize-space()=\"" + productName + "\"]]"
105113
+ "//button");
106-
return waitForClickable(itemButton);
114+
}
115+
116+
/**
117+
* Waits until the card's button reflects the expected state, identified by
118+
* the {@code data-test} prefix ("add-to-cart" or "remove"). This is the
119+
* reliable signal that the click was processed by the app.
120+
*/
121+
private void waitForButtonState(String productName, String dataTestPrefix) {
122+
wait.until(d -> {
123+
var buttons = d.findElements(actionButtonLocator(productName));
124+
if (buttons.isEmpty()) {
125+
return false;
126+
}
127+
String dataTest = buttons.get(0).getDomAttribute("data-test");
128+
return dataTest != null && dataTest.startsWith(dataTestPrefix);
129+
});
107130
}
108131

109132
/** Convenience helper used by sorting tests to verify ordering. */

testng.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
33

44
<!-- Full regression suite: all UI tests plus the Cucumber BDD layer. -->
5-
<suite name="SauceDemo Regression" verbose="1" parallel="classes" thread-count="3">
5+
<!-- Runs sequentially for stability in CI. The framework is parallel-safe -->
6+
<!-- (ThreadLocal driver); set parallel="classes" thread-count="N" to scale. -->
7+
<suite name="SauceDemo Regression" verbose="1">
68

79
<listeners>
810
<listener class-name="com.saucedemo.listeners.TestListener"/>

0 commit comments

Comments
 (0)