Skip to content

Commit b0bd9e2

Browse files
kdaviduikclaude
andcommitted
fix(e2e): make product navigation resilient to sold-out inventory
navigateToFirstProduct was brittle — it clicked the first product link on the page regardless of stock status. When a test store's first product happened to be sold out, addToCart() would fail with a misleading timeout looking for a nonexistent "Add to cart" button. Introduce navigateToInStockProduct which tries product links sequentially, checking for an "Add to cart" button before settling. Sold-out products are skipped. Tests only fail when every product on the page is unavailable. Addresses reviewer feedback from Gray, Kara, and John: - Renamed method to match its actual behavior (navigateToInStockProduct) - Extracted shared "Add to cart" button locator (getAddToCartButton) to eliminate duplication with addToCart() - Switched from has-text CSS selector to getByRole for consistency with project E2E guidelines - Replaced goBack() with goto(originalUrl) for reliable navigation - Replaced networkidle waits with visible-element assertions - Added max-attempts cap (10) to bound failure time - Named the timeout constant with explanatory context A deprecated navigateToFirstProduct alias is kept to avoid breaking any external callers, but all internal call sites are updated. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 351d55d commit b0bd9e2

12 files changed

+65
-24
lines changed

e2e/CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Per [Playwright best practices](https://playwright.dev/docs/best-practices#make-
5656
test.describe('Quantity Management', () => {
5757
test.beforeEach(async ({storefront}) => {
5858
await storefront.goto('/');
59-
await storefront.navigateToFirstProduct();
59+
await storefront.navigateToInStockProduct();
6060
await storefront.addToCart();
6161
});
6262

@@ -70,7 +70,7 @@ test.describe('Quantity Management', () => {
7070
// ACCEPTABLE: Duplicate simple 1-2 line setups when it improves clarity
7171
test('adds item to empty cart', async ({storefront}) => {
7272
await storefront.goto('/');
73-
await storefront.navigateToFirstProduct();
73+
await storefront.navigateToInStockProduct();
7474
await storefront.addToCart();
7575

7676
await expect(storefront.getCartLineItems()).toHaveCount(1);
@@ -79,7 +79,7 @@ test('adds item to empty cart', async ({storefront}) => {
7979
// AVOID: Repeating 3+ lines in every test
8080
test('increases quantity', async ({storefront}) => {
8181
await storefront.goto('/'); // Repeated
82-
await storefront.navigateToFirstProduct(); // Repeated
82+
await storefront.navigateToInStockProduct(); // Repeated
8383
await storefront.addToCart(); // Repeated
8484
// Use beforeEach instead
8585
});

e2e/fixtures/storefront.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -460,22 +460,63 @@ export class StorefrontPage {
460460
}
461461

462462
/**
463-
* Navigate to the first product on the page
464-
*/
463+
* Navigate to an in-stock product by trying product links sequentially.
464+
* Skips sold-out products (no "Add to cart" button) so that tests only
465+
* fail when every product on the page is unavailable.
466+
*/
467+
async navigateToInStockProduct() {
468+
const listingUrl = this.page.url();
469+
const productLinks = this.page.locator('a[href*="/products/"]');
470+
const linkCount = await productLinks.count();
471+
expect(linkCount, 'At least one product link should exist').toBeGreaterThan(
472+
0,
473+
);
474+
475+
// Cap attempts to avoid slow failure on pages with many products
476+
const MAX_PRODUCT_ATTEMPTS = 10;
477+
const attemptsToMake = Math.min(linkCount, MAX_PRODUCT_ATTEMPTS);
478+
// Shorter timeout than addToCart — just checking presence, not waiting
479+
// for a slow action. Long enough for SSR pages to render the button.
480+
const IN_STOCK_CHECK_TIMEOUT_MS = 5000;
481+
482+
for (let i = 0; i < attemptsToMake; i++) {
483+
const link = productLinks.nth(i);
484+
if (!(await link.isVisible())) continue;
485+
486+
await link.click();
487+
488+
const isInStock = await this.getAddToCartButton()
489+
.isVisible({timeout: IN_STOCK_CHECK_TIMEOUT_MS})
490+
.catch(() => false);
491+
492+
if (isInStock) return;
493+
494+
// Product is sold out — return to listing and try the next one
495+
await this.page.goto(listingUrl);
496+
await expect(productLinks.first()).toBeVisible();
497+
}
498+
499+
throw new Error(
500+
`No in-stock products found at ${listingUrl} ` +
501+
`(checked ${attemptsToMake} of ${linkCount} product links). ` +
502+
'All products appear to be sold out.',
503+
);
504+
}
505+
506+
/** @deprecated Use {@link navigateToInStockProduct} instead */
465507
async navigateToFirstProduct() {
466-
const productLink = this.page.locator('a[href*="/products/"]').first();
467-
await expect(productLink).toBeVisible();
468-
await productLink.click();
469-
await this.page.waitForLoadState('networkidle');
508+
return this.navigateToInStockProduct();
509+
}
510+
511+
private getAddToCartButton() {
512+
return this.page.getByRole('button', {name: /add to cart/i});
470513
}
471514

472515
/**
473516
* Click the "Add to cart" button and wait for cart drawer with checkout URL
474517
*/
475518
async addToCart() {
476-
const addToCartButton = this.page.locator(
477-
'button:has-text("Add to cart"), button:has-text("Add to Cart")',
478-
);
519+
const addToCartButton = this.getAddToCartButton();
479520
await expect(addToCartButton).toBeVisible({timeout: 10000});
480521
await addToCartButton.click();
481522

e2e/specs/new-cookies/consent-tracking-accept.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ test.describe('Consent Tracking - Auto-Allowed (Consent Allowed by Default)', ()
8484
await storefront.finalizePerfKitMetrics();
8585

8686
// 7. Navigate to a product (triggers perf-kit to send metrics)
87-
await storefront.navigateToFirstProduct();
87+
await storefront.navigateToInStockProduct();
8888

8989
// Wait for perf-kit to send metrics after visibility change
9090
await storefront.page.waitForTimeout(500);

e2e/specs/new-cookies/consent-tracking-decline.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ test.describe('Consent Tracking - No Banner (Declined by Default)', () => {
5353
storefront.expectNoMonorailRequests();
5454

5555
// 10. Navigate to first product and add to cart
56-
await storefront.navigateToFirstProduct();
56+
await storefront.navigateToInStockProduct();
5757
await storefront.addToCart();
5858

5959
// 11. Check server-timing from cart mutation - should be mock values

e2e/specs/new-cookies/privacy-banner-accept.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ test.describe('Privacy Banner - Accept Flow', () => {
115115
await storefront.finalizePerfKitMetrics();
116116

117117
// 10. Navigate to a product (this triggers perf-kit to send metrics via visibility change)
118-
await storefront.navigateToFirstProduct();
118+
await storefront.navigateToInStockProduct();
119119

120120
// 11. Verify perf-kit payload contains correct tracking values
121121
// Wait a moment for perf-kit to send its metrics after visibility change

e2e/specs/new-cookies/privacy-banner-consent-change.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ test.describe('Privacy Banner - Consent Change', () => {
8383
storefront.expectNoMonorailRequests();
8484

8585
// 11. Navigate to a product page to verify no tracking on navigation
86-
await storefront.navigateToFirstProduct();
86+
await storefront.navigateToInStockProduct();
8787

8888
// Still no analytics requests after navigation
8989
storefront.expectNoMonorailRequests();
@@ -198,7 +198,7 @@ test.describe('Privacy Banner - Consent Change', () => {
198198

199199
// 11. Navigate to a product page
200200
await storefront.finalizePerfKitMetrics();
201-
await storefront.navigateToFirstProduct();
201+
await storefront.navigateToInStockProduct();
202202

203203
// Note: We skip perf-kit request verification here because it captures Y/S values
204204
// only when its script is first downloaded so it won't update the values after changing

e2e/specs/new-cookies/privacy-banner-decline.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ test.describe('Privacy Banner - Decline Flow', () => {
5656
storefront.expectNoMonorailRequests();
5757

5858
// 11. Navigate to first product and add to cart to verify server-timing mock values
59-
await storefront.navigateToFirstProduct();
59+
await storefront.navigateToInStockProduct();
6060

6161
// Add item to cart
6262
await storefront.addToCart();

e2e/specs/old-cookies/consent-tracking-accept.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ test.describe('Consent Tracking - Auto-Allowed (Consent Allowed by Default)', ()
6464
await storefront.finalizePerfKitMetrics();
6565

6666
// 7. Navigate to a product (triggers perf-kit to send metrics)
67-
await storefront.navigateToFirstProduct();
67+
await storefront.navigateToInStockProduct();
6868

6969
// Wait for perf-kit to send metrics after visibility change
7070
await storefront.page.waitForTimeout(500);

e2e/specs/old-cookies/consent-tracking-decline.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ test.describe('Consent Tracking - No Banner (Declined by Default)', () => {
5353
storefront.expectNoMonorailRequests();
5454

5555
// 10. Navigate to first product and add to cart
56-
await storefront.navigateToFirstProduct();
56+
await storefront.navigateToInStockProduct();
5757
await storefront.addToCart();
5858

5959
// 11. Check server-timing from cart mutation - should be mock values

e2e/specs/old-cookies/privacy-banner-accept.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ test.describe('Privacy Banner - Accept Flow', () => {
9696
await storefront.finalizePerfKitMetrics();
9797

9898
// 10. Navigate to a product (this triggers perf-kit to send metrics via visibility change)
99-
await storefront.navigateToFirstProduct();
99+
await storefront.navigateToInStockProduct();
100100

101101
// 11. Verify perf-kit payload contains correct tracking values
102102
// Wait a moment for perf-kit to send its metrics after visibility change

0 commit comments

Comments
 (0)