diff --git a/e2e/scripts/pageHelpers.js b/e2e/scripts/pageHelpers.js index b67b4a6e6c..ba6e01c92b 100644 --- a/e2e/scripts/pageHelpers.js +++ b/e2e/scripts/pageHelpers.js @@ -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 @@ -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(); @@ -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 }); @@ -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(); @@ -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') 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 }) @@ -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(); @@ -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 }) @@ -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; } @@ -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..."]'); diff --git a/e2e/tests/dnt.spec.js b/e2e/tests/desktop/dnt.spec.js similarity index 67% rename from e2e/tests/dnt.spec.js rename to e2e/tests/desktop/dnt.spec.js index 9565205a21..538b0115c6 100644 --- a/e2e/tests/dnt.spec.js +++ b/e2e/tests/desktop/dnt.spec.js @@ -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'; @@ -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"]' @@ -62,6 +37,7 @@ 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; @@ -69,9 +45,8 @@ test("Shopper can use the consent tracking form", async ({ page }) => { 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'); @@ -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(); diff --git a/e2e/tests/desktop/registered-shopper.spec.js b/e2e/tests/desktop/registered-shopper.spec.js index c1c34afa17..bc6aa7fada 100644 --- a/e2e/tests/desktop/registered-shopper.spec.js +++ b/e2e/tests/desktop/registered-shopper.spec.js @@ -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 diff --git a/e2e/tests/homepage.spec.js b/e2e/tests/homepage.spec.js index a8f57c06f3..fe0687f9cd 100644 --- a/e2e/tests/homepage.spec.js +++ b/e2e/tests/homepage.spec.js @@ -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 }) => { diff --git a/e2e/tests/mobile/dnt.spec.js b/e2e/tests/mobile/dnt.spec.js new file mode 100644 index 0000000000..66f72e7748 --- /dev/null +++ b/e2e/tests/mobile/dnt.spec.js @@ -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) => { + 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); +}); diff --git a/e2e/tests/mobile/registered-shopper.spec.js b/e2e/tests/mobile/registered-shopper.spec.js index e751a85ea7..19c134a60f 100644 --- a/e2e/tests/mobile/registered-shopper.spec.js +++ b/e2e/tests/mobile/registered-shopper.spec.js @@ -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(), isMobile: true }) }