Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@W-17682173 Fixing E2E tests failures #2224

Merged
merged 21 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions e2e/scripts/pageHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ const { expect } = require("@playwright/test");
const config = require("../config");
const { getCreditCardExpiry } = require("../scripts/utils.js")

/**
* Give an answer to the consent tracking form.
*
* Note: the consent tracking form hovers over some elements in the app. This can cause a test to fail.
* Run this function after a page.goto to release the form from view.
*
* @param {Object} page - Object that represents a tab/window in the browser provided by playwright
* @param {Boolean} dnt - Do Not Track value to answer the form. False to enable tracking, True to disable tracking.
*/
export const answerConsentTrackingForm = async (page, dnt = false) => {
if (await page.locator('text=Tracking Consent').count() > 0) {
var text = 'Accept'
if (dnt)
text = 'Decline'
const answerButton = page.locator('button:visible', { hasText: text });
await expect(answerButton).toBeVisible();
await answerButton.click();
}
}

/**
* Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on mobile
* with the black variant selected
Expand All @@ -11,6 +31,7 @@ const { getCreditCardExpiry } = require("../scripts/utils.js")
export const navigateToPDPMobile = async ({page}) => {
// Home page
await page.goto(config.RETAIL_APP_HOME);
await answerConsentTrackingForm(page)

await page.getByLabel("Menu", { exact: true }).click();

Expand Down Expand Up @@ -64,6 +85,7 @@ export const navigateToPDPMobile = async ({page}) => {
*/
export const navigateToPDPDesktop = async ({page}) => {
await page.goto(config.RETAIL_APP_HOME);
await answerConsentTrackingForm(page)

await page.getByRole("link", { name: "Womens" }).hover();
const topsNav = await page.getByRole("link", { name: "Tops", exact: true });
Expand Down Expand Up @@ -144,6 +166,7 @@ export const addProductToCart = async ({page, isMobile = false}) => {
export const registerShopper = async ({page, userCredentials, isMobile = false}) => {
// Create Account and Sign In
await page.goto(config.RETAIL_APP_HOME + "/registration");
await answerConsentTrackingForm(page)

await page.waitForLoadState();

Expand All @@ -160,10 +183,13 @@ export const registerShopper = async ({page, userCredentials, isMobile = false})
await page
.locator("input#password")
.fill(userCredentials.password);


// Best Practice: await the network call and assert on the network response rather than waiting for pageLoadState()
// to avoid race conditions from lock in pageLoadState being released before network call resolves
const tokenResponsePromise=page.waitForResponse('**/shopper/auth/v1/organizations/**/oauth2/token')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a best practice, it is always better to await the network call and assert on the network response rather than waiting for pageLoadState(). If an assert after the await for pageLoadState times out, the lock for pageloadstate will be released while the previous network call is still in progress. This will lead to race conditions where if the network call takes longer than the await timeout, the test flow will incorrectly assume the network called failed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this in a comment somewhere so we don't forget to do this in future tests?

await page.getByRole("button", { name: /Create Account/i }).click();

await page.waitForLoadState();
await tokenResponsePromise;
expect((await tokenResponsePromise).status()).toBe(200);

await expect(
page.getByRole("heading", { name: /Account Details/i })
Expand All @@ -186,6 +212,8 @@ export const registerShopper = async ({page, userCredentials, isMobile = false})
*/
export const validateOrderHistory = async ({page}) => {
await page.goto(config.RETAIL_APP_HOME + "/account/orders");
await answerConsentTrackingForm(page)

await expect(
page.getByRole("heading", { name: /Order History/i })
).toBeVisible();
Expand All @@ -209,6 +237,7 @@ export const validateOrderHistory = async ({page}) => {
*/
export const validateWishlist = async ({page}) => {
await page.goto(config.RETAIL_APP_HOME + "/account/wishlist");
await answerConsentTrackingForm(page)

await expect(
page.getByRole("heading", { name: /Wishlist/i })
Expand Down Expand Up @@ -236,19 +265,17 @@ export const validateWishlist = async ({page}) => {
export const loginShopper = async ({page, userCredentials}) => {
try {
await page.goto(config.RETAIL_APP_HOME + "/login");
await answerConsentTrackingForm(page)

await page.locator("input#email").fill(userCredentials.email);
await page
.locator("input#password")
.fill(userCredentials.password);

const tokenResponsePromise=page.waitForResponse('**/shopper/auth/v1/organizations/**/oauth2/token')
await page.getByRole("button", { name: /Sign In/i }).click();

await page.waitForLoadState();

// redirected to Account Details page after logging in
await expect(
page.getByRole("heading", { name: /Account Details/i })
).toBeVisible({ timeout: 2000 });
return true;
await tokenResponsePromise;
return await tokenResponsePromise.status() === 200;
} catch {
return false;
}
Expand All @@ -263,7 +290,7 @@ export const loginShopper = async ({page, userCredentials}) => {
*/
export const searchProduct = async ({page, query, isMobile = false}) => {
await page.goto(config.RETAIL_APP_HOME);

await answerConsentTrackingForm(page)
// For accessibility reasons, we have two search bars
// one for desktop and one for mobile depending on your device type
const searchInputs = page.locator('input[aria-label="Search for products..."]');
Expand Down
47 changes: 11 additions & 36 deletions e2e/tests/dnt.spec.js → e2e/tests/desktop/dnt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,16 @@
*/

const { test, expect } = require("@playwright/test");
const config = require("../config");
const config = require("../../config.js");
const {
generateUserCredentials
} = require("../scripts/utils.js");
} = require("../../scripts/utils.js");
const {
registerShopper
} = require("../../scripts/pageHelpers.js")

const REGISTERED_USER_CREDENTIALS = generateUserCredentials();

const registerUser = async (page) => {
await page.goto(config.RETAIL_APP_HOME + "/registration");

const registrationFormHeading = page.getByText(/Let's get started!/i);
await registrationFormHeading.waitFor();

await page
.locator("input#firstName")
.fill(REGISTERED_USER_CREDENTIALS.firstName);
await page
.locator("input#lastName")
.fill(REGISTERED_USER_CREDENTIALS.lastName);
await page.locator("input#email").fill(REGISTERED_USER_CREDENTIALS.email);
await page
.locator("input#password")
.fill(REGISTERED_USER_CREDENTIALS.password);

await page.getByRole("button", { name: /Create Account/i }).click();

await expect(
page.getByRole("heading", { name: /Account Details/i })
).toBeVisible();

await expect(
page.getByRole("heading", { name: /My Account/i })
).toBeVisible();
}

const checkDntCookie = async (page, expectedValue) => {
var cookies = await page.context().cookies();
var cookieName = 'dw_dnt';
Expand All @@ -49,9 +24,9 @@ const checkDntCookie = async (page, expectedValue) => {
expect(cookie.value).toBe(expectedValue);
}


test("Shopper can use the consent tracking form", async ({ page }) => {
await page.context().clearCookies();

await page.goto(config.RETAIL_APP_HOME);

const modalSelector = '[aria-label="Close consent tracking form"]'
Expand All @@ -62,16 +37,16 @@ test("Shopper can use the consent tracking form", async ({ page }) => {
const declineButton = page.locator('button:visible', { hasText: 'Decline' });
await expect(declineButton).toBeVisible();
await declineButton.click();
await page.waitForTimeout(5000);

// Intercept einstein request
let apiCallsMade = false;
await page.route('https://api.cquotient.com/v3/activities/aaij-MobileFirst/viewCategory', (route) => {
apiCallsMade = true;
route.continue();
});

// The value of 1 comes from defaultDnt prop in _app-config/index.jsx
checkDntCookie(page, '1')

await checkDntCookie(page, '1')

// Trigger einstein events
await page.click('text=Womens');
Expand All @@ -80,8 +55,8 @@ test("Shopper can use the consent tracking form", async ({ page }) => {
await expect(page.getByText(/Tracking Consent/i)).toBeHidden();

// Registering after setting DNT persists the preference
await registerUser(page)
checkDntCookie(page, '1')
await registerShopper({page, userCredentials: REGISTERED_USER_CREDENTIALS});
await checkDntCookie(page, '1')

// Logging out clears the preference
const buttons = await page.getByText(/Log Out/i).elementHandles();
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/desktop/registered-shopper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ test("Registered shopper can add item to wishlist", async ({ page }) => {
})

if(!isLoggedIn) {
await registerShopper({page, userCredentials: REGISTERED_USER_CREDENTIALS})
await registerShopper({page, userCredentials: generateUserCredentials() })
}

// Navigate to PDP
Expand Down
2 changes: 2 additions & 0 deletions e2e/tests/homepage.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

const { test, expect } = require("@playwright/test");
const config = require("../config");
const {answerConsentTrackingForm} = require("../scripts/pageHelpers.js")

test.describe("Retail app home page loads", () => {
test.beforeEach(async ({ page }) => {
await page.goto(config.RETAIL_APP_HOME);
await answerConsentTrackingForm(page);
});

test("has title", async ({ page }) => {
Expand Down
91 changes: 91 additions & 0 deletions e2e/tests/mobile/dnt.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

const { test, expect } = require("@playwright/test");
const config = require("../../config.js");
const {
generateUserCredentials
} = require("../../scripts/utils.js");
const {
registerShopper
} = require("../../scripts/pageHelpers.js")

const REGISTERED_USER_CREDENTIALS = generateUserCredentials();

const checkDntCookie = async (page, expectedValue) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding more test cases to assert DNT flows. 🥳

var cookies = await page.context().cookies();
var cookieName = 'dw_dnt';
var cookie = cookies.find(cookie => cookie.name === cookieName);
expect(cookie).toBeTruthy();
expect(cookie.value).toBe(expectedValue);
}

test("Shopper can use the consent tracking form", async ({ page }) => {
await page.context().clearCookies();

await page.goto(config.RETAIL_APP_HOME);

const modalSelector = '[aria-label="Close consent tracking form"]'
page.locator(modalSelector).waitFor()
await expect(page.getByText(/Tracking Consent/i)).toBeVisible({timeout: 10000});

// Decline Tracking
const declineButton = page.locator('button:visible', { hasText: 'Decline' });
await expect(declineButton).toBeVisible();
await declineButton.click();

// Intercept einstein request
let apiCallsMade = false;
await page.route('https://api.cquotient.com/v3/activities/aaij-MobileFirst/viewCategory', (route) => {
apiCallsMade = true;
route.continue();
});

await checkDntCookie(page, '1')

// Trigger einstein events
await page.getByLabel("Menu", { exact: true }).click();

// SSR nav loads top level categories as direct links so we wait till all sub-categories load in the accordion
const categoryAccordion = page.locator(
"#category-nav .chakra-accordion__button svg+:text('Womens')"
);
await categoryAccordion.waitFor();

await page.getByRole("button", { name: "Womens" }).click();

const clothingNav = page.getByRole("button", { name: "Clothing" });

await clothingNav.waitFor();

await clothingNav.click();
// Reloading the page after setting DNT makes the form not appear again
await page.reload()
await expect(page.getByText(/Tracking Consent/i)).toBeHidden();

// Registering after setting DNT persists the preference
await registerShopper({page, userCredentials: REGISTERED_USER_CREDENTIALS});
await checkDntCookie(page, '1')

// Logging out clears the preference
await page.getByRole("heading", { name: /My Account/i }).click()
const buttons = await page.getByText(/Log Out/i).elementHandles();
for (const button of buttons) {
if (await button.isVisible()) {
await button.click();
break;
}
}

var cookies = await page.context().cookies();
if (cookies.some(item => item.name === "dw_dnt")) {
throw new Error('dw_dnt still exists in the cookies');
}
await page.reload();
await expect(page.getByText(/Tracking Consent/i)).toBeVisible({timeout: 10000});
expect(apiCallsMade).toBe(false);
});
2 changes: 1 addition & 1 deletion e2e/tests/mobile/registered-shopper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ test("Registered shopper can add item to wishlist", async ({ page }) => {
if(!isLoggedIn) {
await registerShopper({
page,
userCredentials: REGISTERED_USER_CREDENTIALS,
userCredentials: generateUserCredentials(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While each individual test runs in isolation from the other tests, we must be mindful that the tests are running against a real deployed instance and the global variables in the test file like REGISTERED_USER_CREDENTIALS will not reset for each test.

When REGISTERED_USER_CREDENTIALS is reused for sign up in multiple tests, all tests after the first one will fail since the user with REGISTERED_USER_CREDENTIALS will already exist on the instance. Hence if you have signup flows in multiple tests, you must always call generateUserCredentials() to avoid conflicting usernames.

isMobile: true
})
}
Expand Down
Loading