diff --git a/e2e/config.js b/e2e/config.js index 4d2e315bb6..663ebe9891 100644 --- a/e2e/config.js +++ b/e2e/config.js @@ -9,7 +9,7 @@ module.exports = { RETAIL_APP_HOME: process.env.RETAIL_APP_HOME || "https://scaffold-pwa-e2e-tests-pwa-kit.mobify-storefront.com", - RETAIL_APP_HOME_SITE: "RefArchGlobal", + RETAIL_APP_HOME_SITE: "RefArch", GENERATED_PROJECTS_DIR: "../generated-projects", GENERATE_PROJECTS: ["retail-app-demo", "retail-app-ext", "retail-app-no-ext"], GENERATOR_CMD: diff --git a/e2e/scripts/pageHelpers.js b/e2e/scripts/pageHelpers.js index 6b3f0e1589..5c55d3b95c 100644 --- a/e2e/scripts/pageHelpers.js +++ b/e2e/scripts/pageHelpers.js @@ -19,25 +19,35 @@ const {getCreditCardExpiry, runAccessibilityTest} = require('../scripts/utils.js */ export const answerConsentTrackingForm = async (page, dnt = false) => { try { - const consentFormVisible = await page.locator('text=Tracking Consent').isVisible().catch(() => false) + const consentFormVisible = await page + .locator('text=Tracking Consent') + .isVisible() + .catch(() => false) if (!consentFormVisible) { return } const buttonText = dnt ? 'Decline' : 'Accept' - await page.getByRole('button', { name: new RegExp(buttonText, 'i') }).first().waitFor({ timeout: 3000 }) - + await page + .getByRole('button', {name: new RegExp(buttonText, 'i')}) + .first() + .waitFor({timeout: 3000}) + // Find and click consent buttons (handles both mobile and desktop versions existing in the DOM) const clickSuccess = await page.evaluate((targetText) => { // Try aria-label first, then fallback to text content - let buttons = Array.from(document.querySelectorAll(`button[aria-label="${targetText} tracking"]`)) - + let buttons = Array.from( + document.querySelectorAll(`button[aria-label="${targetText} tracking"]`) + ) + if (buttons.length === 0) { - buttons = Array.from(document.querySelectorAll('button')).filter(btn => - btn.textContent && btn.textContent.trim().toLowerCase() === targetText.toLowerCase() + buttons = Array.from(document.querySelectorAll('button')).filter( + (btn) => + btn.textContent && + btn.textContent.trim().toLowerCase() === targetText.toLowerCase() ) } - + let clickedCount = 0 buttons.forEach((button) => { // Only click visible buttons @@ -46,14 +56,17 @@ export const answerConsentTrackingForm = async (page, dnt = false) => { clickedCount++ } }) - + return clickedCount }, buttonText) // after clicking an answering button, the tracking consent should not stay in the DOM if (clickSuccess > 0) { await page.waitForTimeout(2000) - await page.locator('text=Tracking Consent').isHidden({ timeout: 5000 }).catch(() => {}) + await page + .locator('text=Tracking Consent') + .isHidden({timeout: 5000}) + .catch(() => {}) } } catch (error) { // Silently continue - consent form handling should not break tests @@ -61,8 +74,8 @@ export const answerConsentTrackingForm = async (page, dnt = false) => { } /** - * Navigates to the `Belted Ribbed Boat Neck Sweater` PDP (Product Detail Page) on mobile - * with the Black variant selected + * Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on mobile + * with the black variant selected * * @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright */ @@ -96,29 +109,28 @@ export const navigateToPDPMobile = async ({page}) => { // PLP const productTile = page.getByRole('link', { - name: /Belted Ribbed Boat Neck Sweater/i + name: /Cotton Turtleneck Sweater/i }) await productTile.scrollIntoViewIfNeeded() // selecting swatch const productTileImg = productTile.locator('img') await productTileImg.waitFor({state: 'visible'}) const initialSrc = await productTileImg.getAttribute('src') - await expect(productTile.getByText(/From \£50\.56/i)).toBeVisible() + await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible() - await productTile.getByLabel(/Black/, {exact: true}).hover() + await productTile.getByLabel(/Black/, {exact: true}).click() // Make sure the image src has changed await expect(async () => { const newSrc = await productTileImg.getAttribute('src') expect(newSrc).not.toBe(initialSrc) }).toPass() - await expect(productTile.getByText(/From \£50\.56/i)).toBeVisible() - + await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible() await productTile.click() } /** - * Navigates to the `Belted Ribbed Boat Neck Sweater` PDP (Product Detail Page) on Desktop - * with the Black variant selected. + * Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on Desktop + * with the black variant selected. * * @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright */ @@ -134,13 +146,13 @@ export const navigateToPDPDesktop = async ({page}) => { // PLP const productTile = page.getByRole('link', { - name: /Belted Ribbed Boat Neck Sweater/i + name: /Cotton Turtleneck Sweater/i }) // selecting swatch const productTileImg = productTile.locator('img') await productTileImg.waitFor({state: 'visible'}) const initialSrc = await productTileImg.getAttribute('src') - await expect(productTile.getByText(/From \£50\.56/i)).toBeVisible() + await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible() await productTile.getByLabel(/Black/, {exact: true}).hover() // Make sure the image src has changed @@ -148,14 +160,14 @@ export const navigateToPDPDesktop = async ({page}) => { const newSrc = await productTileImg.getAttribute('src') expect(newSrc).not.toBe(initialSrc) }).toPass() - await expect(productTile.getByText(/From \£50\.56/i)).toBeVisible() + await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible() await productTile.click() } /** - * Navigates to the `Belted Ribbed Boat Neck Sweater` PDP (Product Detail Page) on Desktop - * with the Black variant selected. + * Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on Desktop + * with the black variant selected. * * @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright */ @@ -188,15 +200,15 @@ export const navigateToPDPDesktopSocial = async ({ } /** - * Adds the `Belted Ribbed Boat Neck Sweater` product to the cart with the variant: - * Colour: Black - * Size: M + * Adds the `Cotton Turtleneck Sweater` product to the cart with the variant: + * Color: Black + * Size: L * * @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright * @param {Boolean} options.isMobile - Flag to indicate if device type is mobile or not, defaulted to false */ export const addProductToCart = async ({page, isMobile = false}) => { - // Navigate to Belted Ribbed Boat Neck Sweater with Black color variant selected + // Navigate to Cotton Turtleneck Sweater with Black color variant selected if (isMobile) { await navigateToPDPMobile({page}) } else { @@ -204,8 +216,8 @@ export const addProductToCart = async ({page, isMobile = false}) => { } // PDP - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() - await page.getByRole('radio', {name: 'M', exact: true}).click() + await expect(page.getByRole('heading', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() + await page.getByRole('radio', {name: 'L', exact: true}).click() await page.locator("button[data-testid='quantity-increment']").click() @@ -213,8 +225,8 @@ export const addProductToCart = async ({page, isMobile = false}) => { // So we need to look at the page URL to verify selected variants const updatedPageURL = await page.url() const params = updatedPageURL.split('?')[1] - expect(params).toMatch(/size=9MD/i) - expect(params).toMatch(/color=JJ3WCXX&/i) + expect(params).toMatch(/size=9LG/i) + expect(params).toMatch(/color=JJ169XX/i) await page.getByRole('button', {name: /Add to Cart/i}).click() const addedToCartModal = page.getByText(/2 items added to cart/i) @@ -250,7 +262,7 @@ export const registerShopper = async ({page, userCredentials, isMobile = false}) const registrationFormHeading = page.getByText(/Let's get started!/i) try { - await registrationFormHeading.waitFor({ timeout: 10000 }) + await registrationFormHeading.waitFor({timeout: 10000}) } catch (error) { // Check if user was redirected to account page during wait const urlAfterWait = page.url() @@ -274,13 +286,13 @@ export const registerShopper = async ({page, userCredentials, isMobile = false}) const tokenResponse = await tokenResponsePromise expect(tokenResponse.status()).toBe(200) - await page.waitForURL(/.*\/account.*/, { timeout: 10000 }) + await page.waitForURL(/.*\/account.*/, {timeout: 10000}) await expect(page.getByText(userCredentials.email)).toBeVisible() } /** - * Validates that the `Belted Ribbed Boat Neck Sweater` product appears in the Order History page + * Validates that the `Cotton Turtleneck Sweater` product appears in the Order History page * * @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright */ @@ -294,15 +306,16 @@ export const validateOrderHistory = async ({page, a11y = {}}) => { await page.getByRole('link', {name: 'View details'}).click() await expect(page.getByRole('heading', {name: /Order Details/i})).toBeVisible() - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() - await expect(page.getByText(/Size: M/i)).toBeVisible() + await expect(page.getByRole('heading', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() + await expect(page.getByText(/Color: Black/i)).toBeVisible() + await expect(page.getByText(/Size: L/i)).toBeVisible() if (checkA11y) { await runAccessibilityTest(page, [snapShotName, 'order-history-a11y-violations.json']) } } /** - * Validates that the `Belted Ribbed Boat Neck Sweater` product appears in the Wishlist page + * Validates that the `Cotton Turtleneck Sweater` product appears in the Wishlist page * * @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright */ @@ -314,9 +327,9 @@ export const validateWishlist = async ({page, a11y = {}}) => { await expect(page.getByRole('heading', {name: /Wishlist/i})).toBeVisible() - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() - await expect(page.getByText(/Colour: Black/i)).toBeVisible() - await expect(page.getByText(/Size: M/i)).toBeVisible() + await expect(page.getByRole('heading', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() + await expect(page.getByText(/Color: Black/i)).toBeVisible() + await expect(page.getByText(/Size: L/i)).toBeVisible() if (checkA11y) { await runAccessibilityTest(page, [snapShotName, 'wishlist-violations.json']) } @@ -349,14 +362,14 @@ export const loginShopper = async ({page, userCredentials}) => { '**/shopper/auth/v1/organizations/**/oauth2/token' ) await page.getByRole('button', {name: /Sign In/i}).click() - + const loginResponse = await loginResponsePromise expect(loginResponse.status()).toBe(303) // Login returns a 303 redirect to /callback with authCode and usid - + const tokenResponse = await tokenResponsePromise expect(tokenResponse.status()).toBe(200) - await page.waitForURL(/.*\/account.*/, { timeout: 10000 }) + await page.waitForURL(/.*\/account.*/, {timeout: 10000}) await expect(page.getByText(userCredentials.email)).toBeVisible() return true @@ -531,7 +544,7 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials, } await answerConsentTrackingForm(page) await page.waitForLoadState() - + // Verify we're on account page and user is logged in const currentUrl = page.url() expect(currentUrl).toMatch(/\/account/) @@ -543,7 +556,7 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials, // cart await page.getByLabel(/My cart/i).click() - await expect(page.getByRole('link', {name: 'Belted Ribbed Boat Neck Sweater'})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() await page.getByRole('link', {name: 'Proceed to Checkout'}).click() @@ -630,7 +643,7 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials, await expect(page.getByRole('heading', {name: /Order Summary/i})).toBeVisible() await expect(page.getByText(/2 Items/i)).toBeVisible() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() if (checkA11y) { await runAccessibilityTest(page, [ 'registered', @@ -643,7 +656,7 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials, /** * Executes the wishlist flow for a registered user. - * + * * Includes robust authentication handling with fallback mechanisms. * * @param {Object} options.page - Playwright page object representing a browser tab/window @@ -688,9 +701,9 @@ export const wishlistFlow = async ({page, registeredUserCredentials, a11y = {}}) await navigateToPDPDesktop({page}) // add product to wishlist - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('heading', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() - await page.getByRole('radio', {name: 'M', exact: true}).click() + await page.getByRole('radio', {name: 'L', exact: true}).click() await page.getByRole('button', {name: /Add to Wishlist/i}).click() // wishlist @@ -699,13 +712,13 @@ export const wishlistFlow = async ({page, registeredUserCredentials, a11y = {}}) /** * Navigates to a PLP and opens the store inventory filter to select a store. - * + * * This helper function demonstrates the store inventory filtering functionality by: * 1. Navigating to the Womens > Tops category PLP * 2. Opening the store locator modal * 3. Searching for stores by postal code * 4. Returning the available store selection options - * + * * This is useful for testing store inventory features and BOPIS (Buy Online, Pick Up In Store) functionality. * * @param {Object} options.page - Playwright page object representing a browser tab/window @@ -720,11 +733,11 @@ export const selectStoreFromPLP = async ({page}) => { // Verify we're on the PLP await expect(page.getByRole('heading', {name: 'Tops'})).toBeVisible() const productTile = page.getByRole('link', { - name: /Belted Ribbed Boat Neck Sweater/i + name: /Cotton Turtleneck Sweater/i }) const productTileImg = productTile.locator('img') await productTileImg.waitFor({state: 'visible'}) - + // Look for the store inventory filter component const storeInventoryFilter = page.getByTestId('sf-store-inventory-filter') await expect(storeInventoryFilter).toBeVisible() @@ -741,24 +754,32 @@ export const selectStoreFromPLP = async ({page}) => { await expect(page.getByText('Find a Store')).toBeVisible() await page.locator('select[name="countryCode"]').selectOption({label: 'United States'}) await page.locator('input[name="postalCode"]').fill('01803') - const findButton = page.getByRole('button', {name: 'Find'}) - await expect(findButton).toBeVisible() - await findButton.click() + const searchStoreButton = page.getByRole('button', {name: 'Find'}) + await expect(searchStoreButton).toBeVisible() - // Wait for stores to load in the modal - await page.waitForLoadState() + const storeSearchResponsePromise = page.waitForResponse( + (resp) => + resp.url().includes('/shopper-stores/v1/organizations/') && + resp.url().includes('/store-search') + ) + await searchStoreButton.click() + const storeSearchResponse = await storeSearchResponsePromise + + expect(storeSearchResponse.status()).toBe(200) // Select the first available store (if any stores are available) await expect(page.getByText(/Burlington Retail Store/i)).toBeVisible() - + // Find and click the first available store label - const storeRadioLabels = page.locator('label.chakra-radio:has(input[aria-describedby^="store-info-"])') + const storeRadioLabels = page.locator( + 'label.chakra-radio:has(input[aria-describedby^="store-info-"])' + ) const storeCount = await storeRadioLabels.count() if (storeCount > 0) { // Select the first store await storeRadioLabels.first().click() - + // Close the store locator modal await page.locator('button[aria-label="Close"]').click() await page.waitForLoadState() @@ -766,7 +787,7 @@ export const selectStoreFromPLP = async ({page}) => { } else { // If no stores are available, verify the appropriate message is shown await expect(page.getByText('Sorry, there are no locations in this area.')).toBeVisible() - + // Close the modal await page.getByRole('button', {name: 'Close'}).click() } diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/cart-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/guest/cart-a11y-violations.json index 4c602307f8..60cbb766ab 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/cart-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/cart-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "
", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rh\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-0.json b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-0.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-0.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-0.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-1.json b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-1.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-1.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-1.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-2.json b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-2.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-2.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-2.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-3.json b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-3.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-3.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-3.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/homepage-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/guest/homepage-a11y-violations.json index 4c602307f8..60cbb766ab 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/homepage-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/homepage-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rh\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/pdp-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/guest/pdp-a11y-violations.json index 4c602307f8..60cbb766ab 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/pdp-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/pdp-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rh\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/guest/plp-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/guest/plp-a11y-violations.json index 4c602307f8..60cbb766ab 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/guest/plp-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/guest/plp-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rh\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/account-addresses-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/registered/account-addresses-a11y-violations.json index 4c602307f8..0e286519ff 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/account-addresses-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/account-addresses-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rf\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/account-details-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/registered/account-details-a11y-violations.json index 4c602307f8..0e286519ff 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/account-details-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/account-details-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rf\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-0.json b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-0.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-0.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-0.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-1.json b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-1.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-1.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-1.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-2.json b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-2.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-2.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-2.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-3.json b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-3.json index a13676961c..b8fefc3110 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-3.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-3.json @@ -23,7 +23,7 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", "target": [ ".css-1k2aozt" diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-4-order-confirmation.json b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-4-order-confirmation.json index 5a7c3e7cf4..3f5bdad047 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-4-order-confirmation.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/checkout-a11y-violations-step-4-order-confirmation.json @@ -7,10 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:r5k\\:" + "#popover-trigger-\\:r5l\\:" ] } ] @@ -23,26 +23,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright", "nodes": [ { - "html": "Free", - "failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.06 (foreground color: #3ba755, background color: #ffffff, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1", + "html": "Continue Shopping", + "failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 4.17 (foreground color: #0176d3, background color: #f3f3f3, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1", "target": [ - ".css-xh9uxa" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:r5k\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + ".css-a4jxtg" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/order-history-a11y-violations.json b/e2e/tests/a11y/desktop/__snapshots__/registered/order-history-a11y-violations.json index 4c602307f8..0e286519ff 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/order-history-a11y-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/order-history-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rf\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/__snapshots__/registered/wishlist-violations.json b/e2e/tests/a11y/desktop/__snapshots__/registered/wishlist-violations.json index 4c602307f8..0e286519ff 100644 --- a/e2e/tests/a11y/desktop/__snapshots__/registered/wishlist-violations.json +++ b/e2e/tests/a11y/desktop/__snapshots__/registered/wishlist-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1l4lb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rf\\:" ] } ] diff --git a/e2e/tests/a11y/desktop/a11y-snapshot-test-guest.spec.js b/e2e/tests/a11y/desktop/a11y-snapshot-test-guest.spec.js index 2f8c8ab60b..8b4f930758 100644 --- a/e2e/tests/a11y/desktop/a11y-snapshot-test-guest.spec.js +++ b/e2e/tests/a11y/desktop/a11y-snapshot-test-guest.spec.js @@ -24,6 +24,9 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { // Handle the consent tracking form using the existing helper await answerConsentTrackingForm(page) + // wait until product tiles are fully load before analyzing + await expect(page.getByRole('link', {name: /Denim slim skirt/i})).toBeVisible() + // Run the a11y test await runAccessibilityTest(page, ['guest', 'homepage-a11y-violations.json']) }) @@ -38,9 +41,9 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { await topsNav.click() const productTile = page.getByRole('link', { - name: /Belted Ribbed Boat Neck Sweater/i + name: /Cotton Turtleneck Sweater/i }) - await expect(productTile.getByText(/From \£50\.56/i)).toBeVisible() + await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible() // Run the a11y test await runAccessibilityTest(page, ['guest', 'plp-a11y-violations.json']) @@ -50,7 +53,7 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { await navigateToPDPDesktop({page}) const getProductPromise = page.waitForResponse( - '**/shopper-products/v1/organizations/**/products/25589266M**', + '**/shopper-products/v1/organizations/**/products/25518241M**', {timeout: 10000} ) @@ -58,8 +61,8 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { const getProductRes = await getProductPromise expect(getProductRes.status()).toBe(200) // ensure that the page is fully loaded before starting a11y scan - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() - await expect(page.getByText(/From \£50\.56/i).nth(1)).toBeVisible() + await expect(page.getByRole('heading', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() + await expect(page.getByText(/From \$39\.99/i).nth(1)).toBeVisible() const addToWishlistButton = page.getByRole('button', {name: /Add to Wishlist/i}) await expect(addToWishlistButton).toBeVisible() @@ -102,7 +105,7 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { await page.waitForLoadState() // make sure the cart is fully load - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() // Run the a11y test await runAccessibilityTest(page, ['guest', 'cart-a11y-violations.json']) @@ -116,7 +119,7 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { await page.waitForLoadState() // make sure the cart is fully load - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() await checkoutProduct({ page, diff --git a/e2e/tests/a11y/mobile/__snapshots__/guest/plp-a11y-violations.json b/e2e/tests/a11y/mobile/__snapshots__/guest/plp-a11y-violations.json index aa6be76556..248b42e145 100644 --- a/e2e/tests/a11y/mobile/__snapshots__/guest/plp-a11y-violations.json +++ b/e2e/tests/a11y/mobile/__snapshots__/guest/plp-a11y-violations.json @@ -7,26 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1lalb9rlbpH1\\:" - ] - } - ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1lalb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" + "#popover-trigger-\\:rn\\:" ] } ] diff --git a/e2e/tests/a11y/mobile/__snapshots__/registered/account-details-a11y-violations.json b/e2e/tests/a11y/mobile/__snapshots__/registered/account-details-a11y-violations.json index 85d798618b..1a601619ad 100644 --- a/e2e/tests/a11y/mobile/__snapshots__/registered/account-details-a11y-violations.json +++ b/e2e/tests/a11y/mobile/__snapshots__/registered/account-details-a11y-violations.json @@ -7,10 +7,10 @@ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/aria-allowed-attr?application=playwright", "nodes": [ { - "html": "", + "html": "", "failureSummary": "Fix all of the following:\n ARIA attribute is not allowed: aria-expanded=\"false\"", "target": [ - "#popover-trigger-\\:R1lalb9rlbpH1\\:" + "#popover-trigger-\\:rl\\:" ] } ] @@ -30,21 +30,5 @@ ] } ] - }, - { - "id": "region", - "impact": "moderate", - "description": "Ensure all page content is contained by landmarks", - "help": "All page content should be contained by landmarks", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/region?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks", - "target": [ - "#popover-trigger-\\:R1lalb9rlbpH1\\: > .css-1igwmid.chakra-stack > .chakra-input__group.css-1y0e7gb > .css-va76oz" - ] - } - ] } ] \ No newline at end of file diff --git a/e2e/tests/a11y/mobile/a11y-snapshot-test-guest.spec.js b/e2e/tests/a11y/mobile/a11y-snapshot-test-guest.spec.js index 2bc4f98e58..27cbfe7222 100644 --- a/e2e/tests/a11y/mobile/a11y-snapshot-test-guest.spec.js +++ b/e2e/tests/a11y/mobile/a11y-snapshot-test-guest.spec.js @@ -40,9 +40,9 @@ test.describe('Accessibility Tests with Snapshots for guest user', () => { // PLP const productTile = page.getByRole('link', { - name: /Belted Ribbed Boat Neck Sweater/i + name: /Cotton Turtleneck Sweater/i }) - await expect(productTile.getByText(/From \£50\.56/i)).toBeVisible() + await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible() // open the filter which has mobile version page.getByRole('button', {name: 'Filter'}).click() diff --git a/e2e/tests/desktop/bopis.spec.js b/e2e/tests/desktop/bopis.spec.js index f84d480e86..2f2779c513 100644 --- a/e2e/tests/desktop/bopis.spec.js +++ b/e2e/tests/desktop/bopis.spec.js @@ -5,12 +5,9 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -const {test, expect, waitFor} = require('@playwright/test') +const {test, expect} = require('@playwright/test') const config = require('../../config.js') -const {generateUserCredentials} = require('../../scripts/utils.js') -const {registerShopper, answerConsentTrackingForm, addProductToCart, checkoutProduct, selectStoreFromPLP} = require('../../scripts/pageHelpers.js') - - +const {answerConsentTrackingForm, selectStoreFromPLP} = require('../../scripts/pageHelpers.js') /** * Test that selecting a store from the store locator sets the PLP filter @@ -40,18 +37,18 @@ test('Adding a product via Pickup in Store to Cart shows pickup address in Check // Go to Men's PLP await page.getByRole('link', {name: 'Mens', exact: true}).hover() - const pantsNav = await page.getByRole('link', {name: 'Pants', exact: true}) + const pantsNav = page.getByRole('link', {name: 'Pants', exact: true}) await expect(pantsNav).toBeVisible() await pantsNav.click() // Navigate to PDP const productTile = page.getByRole('link', { - name: /Refined Denim Pants/i - }) + name: /Casual To Dressy Trousers/i + }).first() await productTile.click() // Select size and Pickup in Store option - await expect(page.getByRole('heading', {name: /Refined Denim Pants/i})).toBeVisible() + await expect(page.getByRole('heading', {name: /Casual To Dressy Trousers/i}).first()).toBeVisible() await page.getByRole('radio', {name: '30'}).click() await page.waitForLoadState() @@ -74,9 +71,6 @@ test('Adding a product via Pickup in Store to Cart shows pickup address in Check await page.getByRole('link', {name: 'View Cart'}).click() await expect(page.getByText(/Order Summary/i)).toBeVisible() -// // Verify the Pickup in Store header is displayed in Cart -// await expect(page.getByText(/Pickup in Store/i)).toBeVisible() - // Proceed to checkout const checkoutButton = page.getByRole('link', {name: 'Proceed to Checkout'}) await expect(checkoutButton).toBeVisible() @@ -90,7 +84,4 @@ test('Adding a product via Pickup in Store to Cart shows pickup address in Check // Confirm the email input toggles to show edit button on clicking "Checkout as guest" const step0Card = page.locator("div[data-testid='sf-toggle-card-step-0']") await expect(step0Card.getByRole('button', {name: /Edit/i})).toBeVisible() - -// // Verify the pickup address is displayed -// await expect(page.getByText(/Burlington Retail Store/i)).toBeVisible() }) diff --git a/e2e/tests/desktop/guest-shopper.spec.js b/e2e/tests/desktop/guest-shopper.spec.js index 67d9eac873..a41f098e25 100644 --- a/e2e/tests/desktop/guest-shopper.spec.js +++ b/e2e/tests/desktop/guest-shopper.spec.js @@ -21,13 +21,13 @@ test('Guest shopper can checkout items as guest', async ({page}) => { // cart await page.getByLabel(/My cart/i).click() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() await checkoutProduct({page, userCredentials: GUEST_USER_CREDENTIALS}) await expect(page.getByRole('heading', {name: /Order Summary/i})).toBeVisible() await expect(page.getByText(/2 Items/i)).toBeVisible() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() }) /** @@ -40,10 +40,10 @@ test('Guest shopper can edit product item in cart', async ({page}) => { await page.getByLabel(/My cart/i).click() await page.waitForLoadState() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() - await expect(page.getByText(/Colour: Black/i)).toBeVisible() - await expect(page.getByText(/Size: M/i)).toBeVisible() + await expect(page.getByText(/Color: Black/i)).toBeVisible() + await expect(page.getByText(/Size: L/i)).toBeVisible() // open product edit modal const editBtn = page.getByRole('button', {name: /Edit/i}) @@ -57,13 +57,13 @@ test('Guest shopper can edit product item in cart', async ({page}) => { // Product edit modal should be open await expect(page.getByTestId('product-view')).toBeVisible() - await page.getByRole('radio', {name: 'L', exact: true}).click() - await page.getByRole('radio', {name: 'New Rattan', exact: true}).click() + await page.getByRole('radio', {name: 'S', exact: true}).click() + await page.getByRole('radio', {name: 'Meadow Violet', exact: true}).click() await page.getByRole('button', {name: /Update/i}).click() await page.waitForLoadState() - await expect(page.getByText(/Size: L/i)).toBeVisible() - await expect(page.getByText(/Colour: New Rattan/i)).toBeVisible() + await expect(page.getByText(/Color: Meadow Violet/i)).toBeVisible() + await expect(page.getByText(/Size: S/i)).toBeVisible() }) /** @@ -99,7 +99,7 @@ test('Guest shopper can checkout product bundle', async ({page}) => { await expect(page.getByText(/Turquoise and Gold Hoop Earring/i)).toBeVisible() const qtyText = page.locator('text="Qty: 1"') - const colorGoldText = page.locator('text="Colour: Gold"') + const colorGoldText = page.locator('text="Color: Gold"') await expect(colorGoldText).toHaveCount(3) await expect(qtyText).toHaveCount(3) diff --git a/e2e/tests/desktop/registered-shopper.spec.js b/e2e/tests/desktop/registered-shopper.spec.js index b5c640694f..b44601ba95 100644 --- a/e2e/tests/desktop/registered-shopper.spec.js +++ b/e2e/tests/desktop/registered-shopper.spec.js @@ -45,16 +45,16 @@ test('Registered shopper can add item to wishlist', async ({page}) => { test.skip('Registered shopper logged in through social retains persisted cart', async ({page}) => { navigateToPDPDesktopSocial({ page, - productName: 'Belted Ribbed Boat Neck Sweater', + productName: 'Floral Ruffle Top', productColor: 'Cardinal Red Multi', productPrice: '£35.19' }) // Add to Cart - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible({ + await expect(page.getByRole('heading', {name: /Floral Ruffle Top/i})).toBeVisible({ timeout: 15000 }) - await page.getByRole('radio', {name: 'M', exact: true}).click() + await page.getByRole('radio', {name: 'L', exact: true}).click() await page.locator("button[data-testid='quantity-increment']").click() @@ -80,7 +80,7 @@ test.skip('Registered shopper logged in through social retains persisted cart', // Check Items in Cart await page.getByLabel(/My cart/i).click() await page.waitForLoadState() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Floral Ruffle Top/i})).toBeVisible() }) export {registeredUserHappyPath} diff --git a/e2e/tests/mobile/guest-shopper.spec.js b/e2e/tests/mobile/guest-shopper.spec.js index 92cb521ba7..cebc7c25fc 100644 --- a/e2e/tests/mobile/guest-shopper.spec.js +++ b/e2e/tests/mobile/guest-shopper.spec.js @@ -22,7 +22,7 @@ test('Guest shopper can checkout items as guest', async ({page}) => { // Cart await page.getByLabel(/My cart/i).click() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() await page.getByRole('link', {name: 'Proceed to Checkout'}).click() @@ -93,7 +93,7 @@ test('Guest shopper can checkout items as guest', async ({page}) => { await expect(page.getByRole('heading', {name: /Order Summary/i})).toBeVisible() await expect(page.getByText(/2 Items/i)).toBeVisible() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() }) /** @@ -105,22 +105,21 @@ test('Guest shopper can edit product item in cart', async ({page}) => { // Cart await page.getByLabel(/My cart/i).click() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() - await expect(page.getByText(/Colour: Black/i)).toBeVisible() - await expect(page.getByText(/Size: M/i)).toBeVisible() + await expect(page.getByText(/Color: Black/i)).toBeVisible() + await expect(page.getByText(/Size: L/i)).toBeVisible() await page.getByRole('button', {name: 'Edit'}).click() await expect(page.getByTestId('product-view')).toBeVisible() // update variant in product edit modal - await page.getByRole('radio', {name: 'L', exact: true}).click() - await page.getByRole('radio', {name: 'New Rattan', exact: true}).click() + await page.getByRole('radio', {name: 'S', exact: true}).click() + await page.getByRole('radio', {name: 'Meadow Violet', exact: true}).click() await page.getByRole('button', {name: /Update/i}).click() - await page.waitForLoadState() - await expect(page.getByText(/Size: L/i)).toBeVisible() - await expect(page.getByText(/Colour: New Rattan/i)).toBeVisible() + await expect(page.getByText(/Color: Meadow Violet/i)).toBeVisible() + await expect(page.getByText(/Size: S/i)).toBeVisible() }) /** @@ -156,7 +155,7 @@ test('Guest shopper can checkout product bundle', async ({page}) => { await expect(page.getByText(/Turquoise and Gold Hoop Earring/i)).toBeVisible() const qtyText = page.locator('text="Qty: 1"') - const colorGoldText = page.locator('text="Colour: Gold"') + const colorGoldText = page.locator('text="Color: Gold"') await expect(colorGoldText).toHaveCount(3) await expect(qtyText).toHaveCount(3) diff --git a/e2e/tests/mobile/registered-shopper.spec.js b/e2e/tests/mobile/registered-shopper.spec.js index 74fd66f864..24c4004154 100644 --- a/e2e/tests/mobile/registered-shopper.spec.js +++ b/e2e/tests/mobile/registered-shopper.spec.js @@ -48,7 +48,7 @@ test('Registered shopper can checkout items', async ({page}) => { await answerConsentTrackingForm(page) await page.waitForLoadState() - + // Verify user is logged in using URL and email verification const currentUrl = page.url() expect(currentUrl).toMatch(/\/account/) @@ -60,7 +60,7 @@ test('Registered shopper can checkout items', async ({page}) => { // cart await page.getByLabel(/My cart/i).click() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() await page.getByRole('link', {name: 'Proceed to Checkout'}).click() @@ -89,7 +89,7 @@ test('Registered shopper can checkout items', async ({page}) => { await expect(page.getByRole('heading', {name: /Shipping & Gift Options/i})).toBeVisible() await page.waitForLoadState() - + // Handle optional shipping step - some checkout flows skip this step const continueToPayment = page.getByRole('button', { name: /Continue to Payment/i @@ -136,7 +136,7 @@ test('Registered shopper can checkout items', async ({page}) => { await expect(page.getByRole('heading', {name: /Order Summary/i})).toBeVisible() await expect(page.getByText(/2 Items/i)).toBeVisible() - await expect(page.getByRole('link', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() + await expect(page.getByRole('link', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() // order history await validateOrderHistory({page}) @@ -160,7 +160,7 @@ test('Registered shopper can add item to wishlist', async ({page}) => { } await answerConsentTrackingForm(page) await page.waitForLoadState() - + // Verify user is logged in using URL and email verification const currentUrl = page.url() expect(currentUrl).toMatch(/\/account/) @@ -170,8 +170,8 @@ test('Registered shopper can add item to wishlist', async ({page}) => { await navigateToPDPMobile({page}) // add product to wishlist - await expect(page.getByRole('heading', {name: /Belted Ribbed Boat Neck Sweater/i})).toBeVisible() - await page.getByRole('radio', {name: 'M', exact: true}).click() + await expect(page.getByRole('heading', {name: /Cotton Turtleneck Sweater/i})).toBeVisible() + await page.getByRole('radio', {name: 'L', exact: true}).click() await page.getByRole('button', {name: /Add to Wishlist/i}).click() // wishlist diff --git a/lerna.json b/lerna.json index 98a9e99932..cb9dda6580 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "packages": [ "packages/*" ] diff --git a/package-lock.json b/package-lock.json index 9e31cb693c..8b653e375a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pwa-kit", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pwa-kit", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "hasInstallScript": true, "dependencies": { "node-fetch": "^2.6.9" diff --git a/package.json b/package.json index 0ebe95ef4c..c7546866c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pwa-kit", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "scripts": { "bump-version": "node ./scripts/bump-version/index.js", "bump-version:retail-react-app": "node ./scripts/bump-version/index.js --package=@salesforce/retail-react-app", diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index 9894b8ebe8..dd3e6bd7bb 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -1,8 +1,4 @@ -## v3.4.0-preview.0 (Jul 11, 2025) -## v3.4.0-dev.0 (Jul 11, 2025) -## v3.11.0-preview.0 (Jul 11, 2025) -## v3.4.0-dev.0 (May 23, 2025) - +## v3.4.0-preview.1 (Jul 18, 2025) - Optionally disable auth init in CommerceApiProvider [#2629](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2629) - Now compatible with either React 17 and 18 [#2506](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2506) - Gracefully handle missing SDK Clients in CommerceApiProvider [#2539](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2539) diff --git a/packages/commerce-sdk-react/package-lock.json b/packages/commerce-sdk-react/package-lock.json index 22ce1799f4..b67278e67a 100644 --- a/packages/commerce-sdk-react/package-lock.json +++ b/packages/commerce-sdk-react/package-lock.json @@ -1,12 +1,12 @@ { "name": "@salesforce/commerce-sdk-react", - "version": "3.4.0-preview.0", + "version": "3.4.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salesforce/commerce-sdk-react", - "version": "3.4.0-preview.0", + "version": "3.4.0-preview.1", "license": "See license in LICENSE", "dependencies": { "commerce-sdk-isomorphic": "^3.3.0", diff --git a/packages/commerce-sdk-react/package.json b/packages/commerce-sdk-react/package.json index 5aedfd50cc..b8f70d4e11 100644 --- a/packages/commerce-sdk-react/package.json +++ b/packages/commerce-sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/commerce-sdk-react", - "version": "3.4.0-preview.0", + "version": "3.4.0-preview.1", "description": "A library that provides react hooks for fetching data from Commerce Cloud", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/ecom-react-hooks#readme", "bugs": { @@ -45,7 +45,7 @@ "jwt-decode": "^4.0.0" }, "devDependencies": { - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", "@tanstack/react-query": "^4.28.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", @@ -60,7 +60,7 @@ "@types/react-helmet": "~6.1.6", "@types/react-router-dom": "~5.3.3", "cross-env": "^5.2.1", - "internal-lib-build": "3.11.0-preview.0", + "internal-lib-build": "3.11.0-preview.1", "jsonwebtoken": "^9.0.0", "nock": "^13.3.0", "nodemon": "^2.0.22", diff --git a/packages/internal-lib-build/package-lock.json b/packages/internal-lib-build/package-lock.json index 7921fd0e4d..6b52c65401 100644 --- a/packages/internal-lib-build/package-lock.json +++ b/packages/internal-lib-build/package-lock.json @@ -1,12 +1,12 @@ { "name": "internal-lib-build", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "internal-lib-build", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@babel/cli": "^7.21.0", diff --git a/packages/internal-lib-build/package.json b/packages/internal-lib-build/package.json index 7c403db685..3e55b77f50 100644 --- a/packages/internal-lib-build/package.json +++ b/packages/internal-lib-build/package.json @@ -1,6 +1,6 @@ { "name": "internal-lib-build", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "private": true, "description": "Build tools for *libraries* in the monorepo", "bugs": { @@ -60,7 +60,7 @@ "shelljs": "^0.9.2" }, "devDependencies": { - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", "npm-packlist": "^4.0.0", "typescript": "4.9.5" }, diff --git a/packages/pwa-kit-create-app/CHANGELOG.md b/packages/pwa-kit-create-app/CHANGELOG.md index 11ecb70f68..504b9d058c 100644 --- a/packages/pwa-kit-create-app/CHANGELOG.md +++ b/packages/pwa-kit-create-app/CHANGELOG.md @@ -1,10 +1,13 @@ -## v3.11.0-dev.0 (May 23, 2025) +## v3.11.0-preview.1 (Jul 18, 2025) +- Fix the demo instance details in `program.json`[#2800](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2800) - Fix exiting before `program.json` content can be flushed [#2699](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2699) - Add `program.json` + Support for Agent-Friendly CLI Input via stdio [#2662](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2662) - Change the default ECOM instance in the generated application [#2610](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2610) - Load active data scripts on demand only [#2623](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2623) - Introduce the cursor rules to assist storefront project developers [#2578](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2578) [#2754](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2754) - Add `StoreLocatorProvider` to the `AppConfig` template to support BOPIS [#2753](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2753) +- Remove search constants overrides so more than 3 products at a time can be fetched [#2819](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2819) +- Update Data Cloud configurations [#2843](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2843) [#2811](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2811) ## v3.10.0 (May 22, 2025) - Add Data Cloud API configuration to `default.js`. [#2318](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2318) diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs index 24adb416a7..0c4977682c 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs @@ -13,17 +13,6 @@ import the underlying constants.js, modifies it and re-export it. */ -import { - DEFAULT_LIMIT_VALUES, - DEFAULT_SEARCH_PARAMS -} from '{{template.source.name}}/app/constants' - -// original value is 25 -DEFAULT_LIMIT_VALUES[0] = 3 -DEFAULT_SEARCH_PARAMS.limit = 3 - export const CUSTOM_HOME_TITLE = '🎉 Hello Extensible React Template!' -export {DEFAULT_LIMIT_VALUES, DEFAULT_SEARCH_PARAMS} - export * from '{{template.source.name}}/app/constants' diff --git a/packages/pwa-kit-create-app/package-lock.json b/packages/pwa-kit-create-app/package-lock.json index 0f77111a9c..d7c896d783 100644 --- a/packages/pwa-kit-create-app/package-lock.json +++ b/packages/pwa-kit-create-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "@salesforce/pwa-kit-create-app", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salesforce/pwa-kit-create-app", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "See license in LICENSE", "dependencies": { "commander": "^9.5.0", diff --git a/packages/pwa-kit-create-app/package.json b/packages/pwa-kit-create-app/package.json index c1cd0eec18..11ed4a597b 100644 --- a/packages/pwa-kit-create-app/package.json +++ b/packages/pwa-kit-create-app/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/pwa-kit-create-app", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "description": "Salesforce's project generator tool", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-create-app#readme", "bugs": { @@ -39,8 +39,8 @@ "tar": "^6.2.1" }, "devDependencies": { - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", - "internal-lib-build": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", + "internal-lib-build": "3.11.0-preview.1", "verdaccio": "^5.22.1" }, "engines": { diff --git a/packages/pwa-kit-create-app/program.json b/packages/pwa-kit-create-app/program.json index 1a62a6419c..79a3962dcf 100644 --- a/packages/pwa-kit-create-app/program.json +++ b/packages/pwa-kit-create-app/program.json @@ -1,4 +1,8 @@ { + "_ai": { + "instruction": "This file defines the CLI behavior for generating React projects using templates and presets. AI agents can use this to guide interactive or programmatic project creation.", + "expectedUse": "Provides schema and data for decision making during app generation." + }, "$schema": "http://json-schema.org/draft-07/schema#", "metadata": { "name": "pwa-kit-create-app", @@ -22,13 +26,21 @@ ], "properties": { "name": { - "type": "string" + "type": "string", + "_ai": { + "instruction": "This value maps directly to the key in the answers object.", + "expectedUse": "Use this to store or retrieve the corresponding answer." + } }, "message": { "type": "string" }, "type": { - "type": "string" + "type": "string", + "_ai": { + "instruction": "This defines the prompt type (e.g., list, input, confirm). Determines how to display the question.", + "expectedUse": "Configure the appropriate UI for asking the question." + } }, "choices": { "type": "array", @@ -44,10 +56,18 @@ }, "value": {} } + }, + "_ai": { + "instruction": "Only used when type is 'list', 'rawlist', 'checkbox', or similar. These are the selectable options.", + "expectedUse": "Render multiple choice options to the user." } } }, "additionalProperties": true + }, + "_ai": { + "instruction": "Defines the questions to be asked to the user or AI agent to gather required input for project generation.", + "expectedUse": "Used to drive interactive or programmatic question-and-answer flows." } }, "presets": { @@ -80,12 +100,16 @@ }, "answers": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "_ai": { + "instruction": "These are default answers pre-filled when this preset is selected. They should be merged with user-supplied answers, where user-supplied values take precedence.", + "expectedUse": "Auto-filling answers to questions without prompting the user unless a value is missing or overridden." + } }, "private": { "type": "boolean", "_ai": { - "instruction": "This property is used to filter what presets are shown to the user. If the preset is private, should NOT be shown to the user in a list of selectable presets. This equates to the 'when' property Inquirer question schema.", + "instruction": "This property is used to filter what presets are shown to the user. If the preset is private, it should NOT be shown to the user in a list of selectable presets.", "expectedUse": "Hiding presets from the user." } } @@ -142,7 +166,11 @@ }, "required": [ "type" - ] + ], + "_ai": { + "instruction": "This defines how the template is sourced. If 'npm', then 'name' must be the NPM package name. If 'bundle', the template is included directly in the CLI.", + "expectedUse": "Used to determine how to fetch and prepare the template for project creation." + } }, "questions": { "$ref": "questions.json", @@ -159,7 +187,11 @@ }, "answers": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "_ai": { + "instruction": "These are default answers pre-filled when this template is selected. They should be merged with user-supplied answers, where user-supplied values take precedence.", + "expectedUse": "Auto-filling answers to questions without prompting the user unless a value is missing or overridden." + } }, "private": { "type": "boolean", @@ -217,6 +249,10 @@ "message", "regex" ] + }, + "_ai": { + "instruction": "These validators can be applied to template questions to ensure valid input. Each validator has a unique `id` that is referenced from questions.", + "expectedUse": "Validating user input for questions during project creation." } }, "options": { @@ -237,7 +273,15 @@ "required": [ "name", "description" - ] + ], + "_ai": { + "instruction": "This option modifies the behavior of the CLI tool. Use `defaultValue` if the option is not provided.", + "expectedUse": "Determine which CLI flags can be included when generating the command." + } + }, + "_ai": { + "instruction": "These are the options/arguments that can be passed to the @salesforce/pwa-kit-create-app CLI tool. Do not use options that aren't listed here.", + "expectedUse": "Instruct agent on how to use the @salesforce/pwa-kit-create-app CLI tool to create a new PWA Kit project." } }, "examples": { @@ -255,7 +299,11 @@ "required": [ "description", "command" - ] + ], + "_ai": { + "instruction": "Each example shows how to use the CLI tool with real command-line input.", + "expectedUse": "Show examples in documentation or when the agent is generating usage examples." + } } } }, @@ -271,16 +319,16 @@ "project.extend": true, "project.hybrid": false, "project.name": "demo-storefront", - "project.commerce.instanceUrl": "https://zzte-053.dx.commercecloud.salesforce.com", - "project.commerce.clientId": "1d763261-6522-4913-9d52-5d947d3b94c4", + "project.commerce.instanceUrl": "https://production-sitegenesis-dw.demandware.net", + "project.commerce.clientId": "44cfcf31-d64d-4227-9cce-1d9b0716c321", "project.commerce.siteId": "RefArch", - "project.commerce.organizationId": "f_ecom_zzte_053", - "project.commerce.shortCode": "kv7kzm78", + "project.commerce.organizationId": "f_ecom_aaia_prd", + "project.commerce.shortCode": "xfdy2axw", "project.commerce.isSlasPrivate": false, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": false }, "private": false @@ -303,8 +351,8 @@ "project.commerce.isSlasPrivate": true, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": true }, "private": true @@ -326,8 +374,8 @@ "project.commerce.isSlasPrivate": false, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": false }, "private": true @@ -349,8 +397,8 @@ "project.commerce.isSlasPrivate": true, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": false }, "private": true @@ -372,8 +420,8 @@ "project.commerce.isSlasPrivate": true, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": false }, "private": true @@ -395,8 +443,8 @@ "project.commerce.isSlasPrivate": true, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": false }, "private": true @@ -418,8 +466,8 @@ "project.commerce.isSlasPrivate": false, "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1", "project.einstein.siteId": "aaij-MobileFirst", - "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e", - "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd", + "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1", + "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd", "project.demo.enableDemoSettings": false }, "private": true @@ -692,4 +740,4 @@ } ] } -} \ No newline at end of file +} diff --git a/packages/pwa-kit-dev/CHANGELOG.md b/packages/pwa-kit-dev/CHANGELOG.md index 008d440c1c..b5798b2158 100644 --- a/packages/pwa-kit-dev/CHANGELOG.md +++ b/packages/pwa-kit-dev/CHANGELOG.md @@ -1,5 +1,4 @@ -## v3.11.0-preview.0 (Jul 11, 2025) -## v3.11.0-dev.0 (May 23, 2025) +## v3.11.0-preview.1 (Jul 18, 2025) ## v3.10.0 (May 22, 2025) - Support source map for both client and server on MRT [#240](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2240) diff --git a/packages/pwa-kit-dev/package-lock.json b/packages/pwa-kit-dev/package-lock.json index f82634c13a..594d72b68e 100644 --- a/packages/pwa-kit-dev/package-lock.json +++ b/packages/pwa-kit-dev/package-lock.json @@ -1,12 +1,12 @@ { "name": "@salesforce/pwa-kit-dev", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salesforce/pwa-kit-dev", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@babel/cli": "^7.21.0", diff --git a/packages/pwa-kit-dev/package.json b/packages/pwa-kit-dev/package.json index 7b7aa1dad0..ae0c8ce7a0 100644 --- a/packages/pwa-kit-dev/package.json +++ b/packages/pwa-kit-dev/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/pwa-kit-dev", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "description": "Build tools for pwa-kit", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-dev#readme", "bugs": { @@ -58,7 +58,7 @@ "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", - "@salesforce/pwa-kit-runtime": "3.11.0-preview.0", + "@salesforce/pwa-kit-runtime": "3.11.0-preview.1", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "archiver": "1.3.0", @@ -121,7 +121,7 @@ "@types/node": "~16.0.3", "@types/node-fetch": "~2.6.3", "@types/validator": "~13.7.14", - "internal-lib-build": "3.11.0-preview.0", + "internal-lib-build": "3.11.0-preview.1", "nock": "^13.3.0", "nodemon": "^2.0.22", "superagent": "^6.1.0", diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md index 8f0c774882..7bb28b901b 100644 --- a/packages/pwa-kit-react-sdk/CHANGELOG.md +++ b/packages/pwa-kit-react-sdk/CHANGELOG.md @@ -1,5 +1,4 @@ -## v3.11.0-preview.0 (Jul 11, 2025) -## v3.11.0-dev.0 (May 23, 2025) +## v3.11.0-preview.1 (Jul 18, 2025) - Fix the performance logging so that it'll capture all SSR queries, even those that result in errors [#2486](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2486) ## v3.10.0 (May 22, 2025) diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json index e61b61dbb6..a6ae5cc7ad 100644 --- a/packages/pwa-kit-react-sdk/package-lock.json +++ b/packages/pwa-kit-react-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@salesforce/pwa-kit-react-sdk", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salesforce/pwa-kit-react-sdk", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@loadable/babel-plugin": "^5.15.3", diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index fef5e4741e..57ea83316b 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/pwa-kit-react-sdk", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "description": "A library that supports the isomorphic React rendering pipeline for Commerce Cloud Managed Runtime apps", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-react-sdk#readme", "bugs": { @@ -37,7 +37,7 @@ "@loadable/babel-plugin": "^5.15.3", "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", - "@salesforce/pwa-kit-runtime": "3.11.0-preview.0", + "@salesforce/pwa-kit-runtime": "3.11.0-preview.1", "@tanstack/react-query": "^4.28.0", "cross-env": "^5.2.1", "event-emitter": "^0.3.5", @@ -50,11 +50,11 @@ }, "devDependencies": { "@loadable/component": "^5.15.3", - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", - "internal-lib-build": "3.11.0-preview.0", + "internal-lib-build": "3.11.0-preview.1", "node-html-parser": "^3.3.6", "nodemon": "^2.0.22", "react": "^18.2.0", diff --git a/packages/pwa-kit-runtime/CHANGELOG.md b/packages/pwa-kit-runtime/CHANGELOG.md index a2c2135161..c24a5e7efc 100644 --- a/packages/pwa-kit-runtime/CHANGELOG.md +++ b/packages/pwa-kit-runtime/CHANGELOG.md @@ -1,5 +1,4 @@ -## v3.11.0-preview.0 (Jul 11, 2025) -## v3.11.0-dev.0 (May 23, 2025) +## v3.11.0-preview.1 (Jul 18, 2025) - Fix the logger so that it will now print out details of the given Error object [#2486](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2486) - Only allow requests for `/shopper/auth/` through the SLAS private client proxy. Also stop the proxy from swallowing SLAS errors [#2608](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2608) diff --git a/packages/pwa-kit-runtime/package-lock.json b/packages/pwa-kit-runtime/package-lock.json index f850366857..87076d34dd 100644 --- a/packages/pwa-kit-runtime/package-lock.json +++ b/packages/pwa-kit-runtime/package-lock.json @@ -1,12 +1,12 @@ { "name": "@salesforce/pwa-kit-runtime", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salesforce/pwa-kit-runtime", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@loadable/babel-plugin": "^5.15.3", diff --git a/packages/pwa-kit-runtime/package.json b/packages/pwa-kit-runtime/package.json index 06c4df68cf..8af06e1a3c 100644 --- a/packages/pwa-kit-runtime/package.json +++ b/packages/pwa-kit-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/pwa-kit-runtime", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "description": "The PWAKit Runtime", "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-runtime#readme", "bugs": { @@ -46,11 +46,11 @@ }, "devDependencies": { "@loadable/component": "^5.15.3", - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", "@serverless/event-mocks": "^1.1.1", "aws-lambda-mock-context": "^3.2.1", "fs-extra": "^11.1.1", - "internal-lib-build": "3.11.0-preview.0", + "internal-lib-build": "3.11.0-preview.1", "nock": "^13.3.0", "nodemon": "^2.0.22", "sinon": "^13.0.2", @@ -58,7 +58,7 @@ "supertest": "^4.0.2" }, "peerDependencies": { - "@salesforce/pwa-kit-dev": "3.11.0-preview.0" + "@salesforce/pwa-kit-dev": "3.11.0-preview.1" }, "peerDependenciesMeta": { "@salesforce/pwa-kit-dev": { diff --git a/packages/pwa-storefront-mcp/jest.config.js b/packages/pwa-storefront-mcp/jest.config.js index 0b5d3d449d..e94965a979 100644 --- a/packages/pwa-storefront-mcp/jest.config.js +++ b/packages/pwa-storefront-mcp/jest.config.js @@ -6,12 +6,15 @@ */ // eslint-disable-next-line @typescript-eslint/no-var-requires const parentConfig = require('@salesforce/pwa-kit-dev/configs/jest/jest.config.js') +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require('path') module.exports = { ...parentConfig, testEnvironment: 'node', testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'], testPathIgnorePatterns: ['bin/*', 'coverage/*', 'dist/*', 'node_modules/*', 'scripts/*'], + setupFilesAfterEnv: [path.join(__dirname, 'jest-setup.js')], collectCoverageFrom: ['src/**/*.js', '!src/**/*.test.js', '!src/**/*.spec.js'], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], diff --git a/packages/pwa-storefront-mcp/package-lock.json b/packages/pwa-storefront-mcp/package-lock.json index a31ab3cb1d..126eb221d1 100644 --- a/packages/pwa-storefront-mcp/package-lock.json +++ b/packages/pwa-storefront-mcp/package-lock.json @@ -1,17 +1,16 @@ { "name": "@salesforce/pwa-kit-storefront-mcp", - "version": "0.1.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salesforce/pwa-kit-storefront-mcp", - "version": "0.1.0", + "version": "3.11.0-preview.1", "license": "ISC", "dependencies": { "@babel/runtime": "^7.21.0", "@modelcontextprotocol/sdk": "^1.13.2", - "node-pty": "1.0.0", "zod": "^3.25.56" }, "devDependencies": { @@ -2282,11 +2281,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/nan": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", - "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==" - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2320,15 +2314,6 @@ "semver": "bin/semver" } }, - "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", - "hasInstallScript": true, - "dependencies": { - "nan": "^2.17.0" - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", diff --git a/packages/pwa-storefront-mcp/package.json b/packages/pwa-storefront-mcp/package.json index 7f5bf44cf6..57af894f10 100644 --- a/packages/pwa-storefront-mcp/package.json +++ b/packages/pwa-storefront-mcp/package.json @@ -46,11 +46,11 @@ "@axe-core/playwright": "^4.10.1", "@babel/node": "^7.22.5", "@playwright/test": "^1.49.0", - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", "axe-core": "^4.10.3", "cross-env": "^5.2.1", "cross-spawn": "^7.0.6", - "internal-lib-build": "3.11.0-preview.0", + "internal-lib-build": "3.11.0-preview.1", "nodemon": "^2.0.22", "playwright": "^1.49.0" }, diff --git a/packages/pwa-storefront-mcp/src/server/server.js b/packages/pwa-storefront-mcp/src/server/server.js index 056ee311fe..76ad5cb4bd 100644 --- a/packages/pwa-storefront-mcp/src/server/server.js +++ b/packages/pwa-storefront-mcp/src/server/server.js @@ -95,7 +95,7 @@ class PwaStorefrontMCPServerHighLevel { this.sessions[sessionId] = {step: 1, answers: {}} } const session = this.sessions[sessionId] - const {step, answers} = session + const {step} = session const answer = args.answer?.trim() switch (step) { case 1: diff --git a/packages/pwa-storefront-mcp/src/server/server.test.js b/packages/pwa-storefront-mcp/src/server/server.test.js index 92d9ba34f7..15b2e65ba4 100644 --- a/packages/pwa-storefront-mcp/src/server/server.test.js +++ b/packages/pwa-storefront-mcp/src/server/server.test.js @@ -37,7 +37,12 @@ describe('PwaStorefrontMCPServerHighLevel integration', () => { it('should list registered tools via stdio', async () => { const child = spawn(BABEL_NODE_PATH, ['src/server/server.js'], { cwd: process.cwd(), - stdio: ['pipe', 'pipe', 'inherit'] + stdio: ['pipe', 'pipe', 'inherit'], + env: { + ...process.env, + // NOTE: THIS ENV VAR IS USUALLY SET BY CURSOR OR THE MCP SERVER? + WORKSPACE_FOLDER_PATHS: path.resolve(process.cwd(), '..', '..') + } }) // Wait a moment for the server to start diff --git a/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js b/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js index e7d1b8d5aa..da7f3bcbe5 100644 --- a/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js +++ b/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.js @@ -139,7 +139,6 @@ export default ${pascalComponentName}; const componentDir = path.join(location, kebabDirName) await fs.mkdir(componentDir, {recursive: true}) const componentFilePath = path.join(componentDir, 'index.jsx') - const fields = Object.keys(dataModel) let code = '' // Special logic for product entity diff --git a/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.test.js b/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.test.js index 437564d87d..eb0e7a7964 100644 --- a/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.test.js +++ b/packages/pwa-storefront-mcp/src/utils/create-new-component-tool.test.js @@ -6,7 +6,6 @@ */ import CreateNewComponentTool from './create-new-component-tool.js' import * as fs from 'fs/promises' -import path from 'path' // Mock fs/promises to avoid actual file operations jest.mock('fs/promises', () => ({ diff --git a/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js b/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js index eeb44849ee..5515a04a06 100644 --- a/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js +++ b/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.js @@ -4,19 +4,13 @@ * 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 */ -import os from 'os' -import path from 'path' -import {exec} from 'child_process' -import fs from 'fs/promises' // Project dependencies -import {EmptyJsonSchema, runNpxCommand} from './utils' +import {EmptyJsonSchema, getCreateAppCommand, isMonoRepo, runCommand} from './utils' -//const CREATE_APP_VERSION = 'latest' -//const CREATE_APP_VERSION = '3.11.0-nightly-20250630080227' -const CREATE_APP_COMMAND = '@salesforce/pwa-kit-create-app@3.11.0-nightly-20250630080227' -const DISPLAY_PROGRAM_COMMAND = '--displayProgram' -const NPX_COMMAND = 'npx' +const CREATE_APP_COMMAND = getCreateAppCommand() +const DISPLAY_PROGRAM_FLAG = '--displayProgram' +const COMMAND_RUNNER = isMonoRepo() ? 'node' : 'npx' const guidelinesText = ` # PWA Kit Create App — Agent Usage Guidelines @@ -63,7 +57,8 @@ If the user requests a project using a **template**: - Never attempt to create a project without using this tool. - When gathering answers for a template, ask questions one at a time to maintain clarity. - Presets and templates are mutually exclusive paths. Do not offer both options unless explicitly requested. -- Use the \`${NPX_COMMAND}\` command to run the \`${CREATE_APP_COMMAND}\` CLI tool when creating a new project. +- Do not pass any flags to the \`${CREATE_APP_COMMAND}\` CLI tool that are not listed in the program.json options". +- Use the \`${COMMAND_RUNNER}\` command to run the \`${CREATE_APP_COMMAND}\` CLI tool when creating a new project. ` export default { @@ -71,18 +66,12 @@ export default { description: `This tool is used to provide the agent with the instructions on how to use the @salesforce/pwa-kit-create-app CLI tool to create a new PWA Kit projects. Do not attempt to create a project without using this tool first.`, inputSchema: EmptyJsonSchema, fn: async () => { - let programOutput = '' - // Run the display program and get the output. - try { - programOutput = await runNpxCommand( - NPX_COMMAND, - CREATE_APP_COMMAND, - DISPLAY_PROGRAM_COMMAND - ) - } catch (err) { - console.error('Failed to run display program:', err) - } + const programOutput = await runCommand(COMMAND_RUNNER, [ + ...(COMMAND_RUNNER === 'npx' ? ['--yes'] : []), + CREATE_APP_COMMAND, + DISPLAY_PROGRAM_FLAG + ]) // Parse the output and get the data, metadata, and schemas. const { diff --git a/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.test.js b/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.test.js new file mode 100644 index 0000000000..ccc7bfbc93 --- /dev/null +++ b/packages/pwa-storefront-mcp/src/utils/pwa-create-app-guideline-tool.test.js @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025, 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 + */ +import CreateAppGuidelineTool from './pwa-create-app-guideline-tool' +import {EmptyJsonSchema} from './utils' + +jest.mock('./utils', () => { + const originalModule = jest.requireActual('./utils') + // eslint-disable-next-line @typescript-eslint/no-var-requires + const path = require('path') + const mockScriptPath = path.resolve('../pwa-kit-create-app/scripts/create-mobify-app.js') + + return { + ...originalModule, + isMonoRepo: jest.fn(() => true), + getCreateAppCommand: jest.fn(() => mockScriptPath) + } +}) + +describe('PWA Create App Guidelines', () => { + describe('CreateAppGuidelineTool', () => { + it('should have correct structure', () => { + expect(CreateAppGuidelineTool).toMatchObject({ + name: 'create_app_guidelines', + description: `This tool is used to provide the agent with the instructions on how to use the @salesforce/pwa-kit-create-app CLI tool to create a new PWA Kit projects. Do not attempt to create a project without using this tool first.`, + inputSchema: EmptyJsonSchema, + fn: expect.any(Function) + }) + }) + + it('should return guidelines content when executed', async () => { + // NOTE: THIS TEST IS SIMPLY A SANITY CHECK TO ENSURE THE TOOL IS WORKING. + // IT DOES NOT TEST THE CONTENT OF THE GUIDELINES IN ITS ENTIRETY. + const result = await CreateAppGuidelineTool.fn() + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: expect.stringContaining('PWA Kit Create App — Agent Usage Guidelines') + } + ] + }) + }) + + it('should include all major sections in the guidelines', async () => { + const result = await CreateAppGuidelineTool.fn() + const guidelineText = result.content[0].text + + const requiredSections = [ + 'Overview', + 'General Rules', + 'Creating a Project Using a Preset', + 'Creating a Project Using a Template', + 'Important Reminders' + ] + + requiredSections.forEach((section) => { + expect(guidelineText).toContain(section) + }) + }) + }) +}) + +afterAll(() => { + delete process.env.WORKSPACE_FOLDER_PATHS +}) diff --git a/packages/pwa-storefront-mcp/src/utils/utils.js b/packages/pwa-storefront-mcp/src/utils/utils.js index 394c7ceab6..332a924eb3 100644 --- a/packages/pwa-storefront-mcp/src/utils/utils.js +++ b/packages/pwa-storefront-mcp/src/utils/utils.js @@ -9,8 +9,10 @@ import path from 'path' import {spawn} from 'cross-spawn' import {zodToJsonSchema} from 'zod-to-json-schema' import {z} from 'zod' -import os from 'os' -import {exec} from 'child_process' + +// CONSTANTS +// const CREATE_APP_VERSION = 'latest' +const CREATE_APP_VERSION = '3.11.0-nightly-20250710080214' // Private schema used to generate the JSON schema const emptySchema = z.object({}).strict() @@ -79,35 +81,26 @@ export const runCommand = async (command, args = [], options = {}) => { /** * Checks if the project is a monorepo by verifying the existence of lerna.json in the root directory. * - * @returns {boolean} True if lerna.json exists in the '../../../..' folder, false otherwise. + * @returns {boolean} True if lerna.json exists in the current workspace, false otherwise. */ export function isMonoRepo() { - const lernaPath = path.resolve(__dirname, '../../../..', 'lerna.json') + const lernaPath = path.resolve(process.env.WORKSPACE_FOLDER_PATHS, 'lerna.json') return fs.existsSync(lernaPath) } /** - * Runs an NPX command and captures its output. + * Returns the command or path to use for creating a new PWA Kit app. + * + * If the project is a monorepo (detected by the presence of lerna.json), + * it returns the absolute path to the local create-mobify-app.js script. + * Otherwise, it returns the npm package name with a specific version. * - * @returns {Promise} - Resolves with the command output. + * @returns {string} The command or path to use for app creation. */ -export async function runNpxCommand(NPX_COMMAND, CREATE_APP_COMMAND, DISPLAY_PROGRAM_COMMAND) { - return new Promise((resolve, reject) => { - const tempDir = os.tmpdir() - const outputFilePath = path.join(tempDir, 'npx-output.json') - const errorFilePath = path.join(tempDir, 'npx-error.log') - const command = `${NPX_COMMAND} ${CREATE_APP_COMMAND} ${DISPLAY_PROGRAM_COMMAND} > ${outputFilePath} 2> ${errorFilePath}` - - exec(command, (error) => { - if (error) { - reject(error) - return - } - - fs.promises - .readFile(outputFilePath, 'utf-8') - .then((data) => resolve(data)) - .catch((err) => reject(err)) - }) - }) +export const getCreateAppCommand = () => { + return isMonoRepo() + ? path.resolve( + `${process.env.WORKSPACE_FOLDER_PATHS}/packages/pwa-kit-create-app/scripts/create-mobify-app.js` + ) + : `@salesforce/pwa-kit-create-app@${CREATE_APP_VERSION}` } diff --git a/packages/pwa-storefront-mcp/src/utils/utils.test.js b/packages/pwa-storefront-mcp/src/utils/utils.test.js index 3a02589ae0..9d0f48b96c 100644 --- a/packages/pwa-storefront-mcp/src/utils/utils.test.js +++ b/packages/pwa-storefront-mcp/src/utils/utils.test.js @@ -4,8 +4,9 @@ * 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 */ -import {EmptyJsonSchema, isMonoRepo} from './utils' +import {EmptyJsonSchema, getCreateAppCommand, isMonoRepo} from './utils' import fs from 'fs' +import path from 'path' describe('Utils', () => { describe('EmptyJsonSchema', () => { @@ -31,6 +32,19 @@ describe('Utils', () => { }) describe('isMonoRepo', () => { + const originalEnv = process.env.WORKSPACE_FOLDER_PATHS + const mockPath = '/mock/root' + + beforeEach(() => { + jest.clearAllMocks() + process.env.WORKSPACE_FOLDER_PATHS = mockPath + }) + + afterEach(() => { + process.env.WORKSPACE_FOLDER_PATHS = originalEnv + jest.restoreAllMocks() + }) + test('returns true if lerna.json exists', () => { jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true) expect(isMonoRepo()).toBe(true) @@ -40,9 +54,34 @@ describe('Utils', () => { jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false) expect(isMonoRepo()).toBe(false) }) + }) + + describe('getCreateAppCommand', () => { + const originalEnv = process.env.WORKSPACE_FOLDER_PATHS + const mockPath = '/mock/root' + const mockScriptPath = `${mockPath}/packages/pwa-kit-create-app/scripts/create-mobify-app.js` + const CREATE_APP_VERSION = '3.11.0-nightly-20250710080214' + + beforeEach(() => { + jest.clearAllMocks() + process.env.WORKSPACE_FOLDER_PATHS = mockPath + }) afterEach(() => { + process.env.WORKSPACE_FOLDER_PATHS = originalEnv jest.restoreAllMocks() }) + + test('returns local script path if monorepo', () => { + jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true) + const result = getCreateAppCommand() + expect(result).toBe(path.resolve(mockScriptPath)) + }) + + test('returns npm package with version if not monorepo', () => { + jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false) + const result = getCreateAppCommand() + expect(result).toBe(`@salesforce/pwa-kit-create-app@${CREATE_APP_VERSION}`) + }) }) }) diff --git a/packages/template-express-minimal/package-lock.json b/packages/template-express-minimal/package-lock.json index b4d15d16e8..b3df254912 100644 --- a/packages/template-express-minimal/package-lock.json +++ b/packages/template-express-minimal/package-lock.json @@ -1,12 +1,12 @@ { "name": "template-express-minimal", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "template-express-minimal", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "See license in LICENSE", "devDependencies": { "supertest": "^4.0.2" diff --git a/packages/template-express-minimal/package.json b/packages/template-express-minimal/package.json index 3e25da9ae2..d1b658bae8 100644 --- a/packages/template-express-minimal/package.json +++ b/packages/template-express-minimal/package.json @@ -1,6 +1,6 @@ { "name": "template-express-minimal", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "private": true, "license": "See license in LICENSE", "scripts": { @@ -15,8 +15,8 @@ "test": "pwa-kit-dev test" }, "devDependencies": { - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", - "@salesforce/pwa-kit-runtime": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", + "@salesforce/pwa-kit-runtime": "3.11.0-preview.1", "supertest": "^4.0.2" }, "mobify": { diff --git a/packages/template-mrt-reference-app/package-lock.json b/packages/template-mrt-reference-app/package-lock.json index 8aa1830247..f042996afe 100644 --- a/packages/template-mrt-reference-app/package-lock.json +++ b/packages/template-mrt-reference-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "template-mrt-reference-app", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "template-mrt-reference-app", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "license": "See license in LICENSE", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.450.0", diff --git a/packages/template-mrt-reference-app/package.json b/packages/template-mrt-reference-app/package.json index 6713052371..322723d556 100644 --- a/packages/template-mrt-reference-app/package.json +++ b/packages/template-mrt-reference-app/package.json @@ -1,6 +1,6 @@ { "name": "template-mrt-reference-app", - "version": "3.11.0-preview.0", + "version": "3.11.0-preview.1", "private": true, "license": "See license in LICENSE", "scripts": { @@ -16,8 +16,8 @@ }, "devDependencies": { "@loadable/component": "^5.15.3", - "@salesforce/pwa-kit-dev": "3.11.0-preview.0", - "@salesforce/pwa-kit-runtime": "3.11.0-preview.0", + "@salesforce/pwa-kit-dev": "3.11.0-preview.1", + "@salesforce/pwa-kit-runtime": "3.11.0-preview.1", "@smithy/smithy-client": "^2.1.15", "aws-sdk-client-mock": "^3.0.0", "cross-fetch": "^3.1.4", diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index 3c44d3b6c8..4dafe90cbe 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,4 +1,4 @@ -## v7.0.0-dev.0 (May 20, 2025) +## v7.0.0-preview.1 (July 18, 2025) - Improved the layout of product tiles in product scroll and product list [#2446](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2446) - Update the configuration of datacloud [#2467](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2467) @@ -7,18 +7,19 @@ - Fix Einstein event tracking for `addToCart` event [#2558](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2558) - Password Reset and Passwordless Integration Test [#2669](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2669) - Update latest translations for all languages [#2616](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2616) -- Added support for Buy Online Pick up In Store (BOPIS) [#2646](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2646) +- Added support for Buy Online Pick up In Store (BOPIS) [#2646](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2646) [#2716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2716) [#2726](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2726) [#2629](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2629) [#2823](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2823) - Load active data scripts on demand only [#2623](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2623) - Provide base image for convenient perf optimizations [#2642](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2642) - Support saving billing phone number on user registration from order confirmation [#2653](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2653) - Support saving default shipping address on user registration from order confirmation [#2706](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2706) - Minor updates to support BOPIS E2E tests [#2716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2716) -- Provide support for partial hydration [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696) -- Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704) -- Support Standard Products [#2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697) +- Provide conditional support for partial hydration (feature flag `PARTIAL_HYDRATION_ENABLED`) [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696) [#2846](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2846) +- Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704) [#2760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2760) [#2815](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2815) +- [Breaking] Support Standard Products [2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697) - Introduce store locator [#2542](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2542) -- Move product items on cart page into a new component [#2760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2760) - +- Fix passwordless race conditions in form submission [#2758](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2758) +- Use `` element for responsive images [#2724](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2724) +- Add Data Cloud partyIdentification events and improve error handling [#2811](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2811) ## v6.1.0 (May 22, 2025) @@ -456,4 +457,4 @@ The versions published below were not published on npm, and the versioning match ### v1.0.0 (Sep 08, 2021) -- PWA Kit General Availability and open source. 🎉 \ No newline at end of file +- PWA Kit General Availability and open source. 🎉 diff --git a/packages/template-retail-react-app/app/components/dynamic-image/index.jsx b/packages/template-retail-react-app/app/components/dynamic-image/index.jsx index 599431ff00..d749cf7260 100644 --- a/packages/template-retail-react-app/app/components/dynamic-image/index.jsx +++ b/packages/template-retail-react-app/app/components/dynamic-image/index.jsx @@ -1,37 +1,112 @@ /* - * Copyright (c) 2021, salesforce.com, inc. + * Copyright (c) 2025, 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 */ import React, {useMemo} from 'react' +import {Helmet} from 'react-helmet' import PropTypes from 'prop-types' import {Box, useTheme} from '@salesforce/retail-react-app/app/components/shared/ui' -import Image from '@salesforce/retail-react-app/app/components/image' -import {getResponsiveImageAttributes} from '@salesforce/retail-react-app/app/utils/responsive-image' +import {Img} from '@salesforce/retail-react-app/app/components/shared/ui' +import {getResponsivePictureAttributes} from '@salesforce/retail-react-app/app/utils/responsive-image' +import { + getImageAttributes, + getImageLinkAttributes +} from '@salesforce/retail-react-app/app/utils/image' +import {isServer} from '@salesforce/retail-react-app/app/components/image/utils' /** - * Quickly create a responsive image using your Dynamic Imaging Service - * @example - * // Widths without a unit are interpreted as px values - * - * - * // You can also use units of px or vw - * + * Responsive image component optimized to work with the Dynamic Imaging Service. + * Via this component it's easy to create a `` element with related + * theme-aware `` elements and responsive preloading for high-priority + * images. + * @example Widths without a unit defined as array (interpreted as px values) + * + * @example Widths without a unit defined as object (interpreted as px values) + * + * @example Widths with mixed px and vw units defined as array + * + * @example Eagerly load image with high priority and responsive preloading + * + * @see {@link https://web.dev/learn/design/responsive-images} + * @see {@link https://web.dev/learn/design/picture-element} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/picture} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images} * @see {@link https://help.salesforce.com/s/articleView?id=cc.b2c_image_transformation_service.htm&type=5} */ const DynamicImage = ({src, widths, imageProps, as, ...rest}) => { - const Component = as ? as : Image + const Component = as ? as : Img const theme = useTheme() - const responsiveImageProps = useMemo( - () => getResponsiveImageAttributes({src, widths, breakpoints: theme.breakpoints}), - [src, widths, theme.breakpoints] - ) + const [responsiveImageProps, numSources, effectiveImageProps, responsiveLinks] = useMemo(() => { + const responsiveImageProps = getResponsivePictureAttributes({ + src, + widths, + breakpoints: theme.breakpoints + }) + const effectiveImageProps = getImageAttributes(imageProps) + const fetchPriority = effectiveImageProps.fetchPriority + const responsiveLinks = + !responsiveImageProps.links.length && fetchPriority === 'high' + ? [ + getImageLinkAttributes({ + ...effectiveImageProps, + fetchPriority, // React <18 vs. >=19 issue + src: responsiveImageProps.src + }) + ] + : responsiveImageProps.links.reduce((acc, link) => { + const linkProps = getImageLinkAttributes({ + ...effectiveImageProps, + ...link, + fetchPriority, // React <18 vs. >=19 issue + src: responsiveImageProps.src + }) + if (linkProps) { + acc.push(linkProps) + } + return acc + }, []) + return [ + responsiveImageProps, + responsiveImageProps.sources.length, + effectiveImageProps, + responsiveLinks + ] + }, [src, widths, theme.breakpoints]) return ( - + {numSources > 0 ? ( + + {responsiveImageProps.sources.map(({srcSet, sizes, media}, idx) => ( + + ))} + + + ) : ( + + )} + + {isServer() && responsiveLinks.length > 0 && ( + + {responsiveLinks.map((responsiveLinkProps, idx) => { + const {href, ...rest} = responsiveLinkProps + return + })} + + )} ) } diff --git a/packages/template-retail-react-app/app/components/dynamic-image/index.test.js b/packages/template-retail-react-app/app/components/dynamic-image/index.test.js index 8c60af940e..e0b2811aa6 100644 --- a/packages/template-retail-react-app/app/components/dynamic-image/index.test.js +++ b/packages/template-retail-react-app/app/components/dynamic-image/index.test.js @@ -7,7 +7,7 @@ /* eslint-disable jest/no-conditional-expect */ import React from 'react' import {Helmet} from 'react-helmet' -import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image/index' +import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image' import {Img} from '@salesforce/retail-react-app/app/components/shared/ui' import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' import {isServer} from '@salesforce/retail-react-app/app/components/image/utils' @@ -26,19 +26,23 @@ const imageProps = { describe('Dynamic Image Component', () => { test('renders an image without decoding strategy and fetch priority', () => { - const {getAllByTitle} = renderWithProviders( - + const {getByTestId, getAllByTitle} = renderWithProviders( + ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).not.toHaveAttribute('decoding') expect(elements[0]).not.toHaveAttribute('fetchpriority') + expect(wrapper.firstElementChild).toBe(elements[0]) }) describe('loading="lazy"', () => { test('renders an image using the default "async" decoding strategy', () => { - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).toHaveAttribute('decoding', 'async') + expect(wrapper.firstElementChild).toBe(elements[0]) }) test.each(['sync', 'async', 'auto'])( 'renders an image using an explicit "%s" decoding strategy', (decoding) => { - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).toHaveAttribute('decoding', decoding) + expect(wrapper.firstElementChild).toBe(elements[0]) } ) test('renders an image replacing an invalid decoding strategy with the default "async" value', () => { - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).toHaveAttribute('decoding', 'async') + expect(wrapper.firstElementChild).toBe(elements[0]) }) - test('renders an explicitly given image component without attribute modifications', () => { - const {getAllByTitle} = renderWithProviders( + test('renders an explicitly given image component', () => { + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) - expect(elements[0]).not.toHaveAttribute('decoding') + expect(elements[0]).toHaveAttribute('decoding', 'async') + expect(wrapper.firstElementChild).toBe(elements[0]) + }) + + test('renders an image with explicit widths', () => { + const {getByTestId, getAllByTitle} = renderWithProviders( + + ) + + const wrapper = getByTestId('dynamic-image') + const elements = getAllByTitle(imageProps.title) + expect(elements).toHaveLength(1) + expect(elements[0]).toHaveAttribute('src', src) + expect(elements[0]).toHaveAttribute('loading', 'lazy') + expect(elements[0]).toHaveAttribute('decoding', 'async') + expect(elements[0]).not.toHaveAttribute('sizes') + expect(elements[0]).not.toHaveAttribute('srcset') + + expect(wrapper.firstElementChild).not.toBe(elements[0]) + expect(wrapper.firstElementChild.tagName.toLowerCase()).toBe('picture') + + const sourceElements = Array.from(wrapper.querySelectorAll('source')) + expect(sourceElements).toHaveLength(5) + expect(sourceElements[0]).toHaveAttribute('media', '(min-width: 80em)') + expect(sourceElements[0]).toHaveAttribute('sizes', '25vw') + expect(sourceElements[0]).toHaveAttribute( + 'srcset', + [384, 768].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[1]).toHaveAttribute('media', '(min-width: 62em)') + expect(sourceElements[1]).toHaveAttribute('sizes', '20vw') + expect(sourceElements[1]).toHaveAttribute( + 'srcset', + [256, 512].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[2]).toHaveAttribute('media', '(min-width: 48em)') + expect(sourceElements[2]).toHaveAttribute('sizes', '20vw') + expect(sourceElements[2]).toHaveAttribute( + 'srcset', + [198, 396].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[3]).toHaveAttribute('media', '(min-width: 30em)') + expect(sourceElements[3]).toHaveAttribute('sizes', '50vw') + expect(sourceElements[3]).toHaveAttribute( + 'srcset', + [384, 768].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[4]).not.toHaveAttribute('media') + expect(sourceElements[4]).toHaveAttribute('sizes', '50vw') + expect(sourceElements[4]).toHaveAttribute( + 'srcset', + [240, 480].map((width) => `${src} ${width}w`).join(', ') + ) + + expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([]) }) }) describe('loading="eager"', () => { test('renders an image using the default "high" fetch priority', () => { - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { widths={['50vw', '50vw', '20vw', '20vw', '25vw']} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) + expect(elements[0]).toHaveAttribute('src', src) + expect(elements[0]).toHaveAttribute('loading', 'eager') expect(elements[0]).toHaveAttribute('fetchpriority', 'high') + expect(elements[0]).not.toHaveAttribute('sizes') + expect(elements[0]).not.toHaveAttribute('srcset') + + expect(wrapper.firstElementChild).not.toBe(elements[0]) + expect(wrapper.firstElementChild.tagName.toLowerCase()).toBe('picture') + + const sourceElements = Array.from(wrapper.querySelectorAll('source')) + expect(sourceElements).toHaveLength(5) + expect(sourceElements[0]).toHaveAttribute('media', '(min-width: 80em)') + expect(sourceElements[0]).toHaveAttribute('sizes', '25vw') + expect(sourceElements[0]).toHaveAttribute( + 'srcset', + [384, 768].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[1]).toHaveAttribute('media', '(min-width: 62em)') + expect(sourceElements[1]).toHaveAttribute('sizes', '20vw') + expect(sourceElements[1]).toHaveAttribute( + 'srcset', + [256, 512].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[2]).toHaveAttribute('media', '(min-width: 48em)') + expect(sourceElements[2]).toHaveAttribute('sizes', '20vw') + expect(sourceElements[2]).toHaveAttribute( + 'srcset', + [198, 396].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[3]).toHaveAttribute('media', '(min-width: 30em)') + expect(sourceElements[3]).toHaveAttribute('sizes', '50vw') + expect(sourceElements[3]).toHaveAttribute( + 'srcset', + [384, 768].map((width) => `${src} ${width}w`).join(', ') + ) + expect(sourceElements[4]).not.toHaveAttribute('media') + expect(sourceElements[4]).toHaveAttribute('sizes', '50vw') + expect(sourceElements[4]).toHaveAttribute( + 'srcset', + [240, 480].map((width) => `${src} ${width}w`).join(', ') + ) const helmet = Helmet.peek() - expect(helmet.linkTags).toHaveLength(1) - expect(helmet.linkTags[0]).toStrictEqual({ - as: 'image', - href: src, - rel: 'preload', - imageSizes: - '(min-width: 80em) 25vw, (min-width: 62em) 20vw, (min-width: 48em) 20vw, (min-width: 30em) 50vw, 50vw', - imageSrcSet: - 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 198w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 396w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 240w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 480w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 256w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 512w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 384w, https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg 768w' - }) + expect(helmet.linkTags).toHaveLength(5) + expect(helmet.linkTags).toStrictEqual([ + { + rel: 'preload', + as: 'image', + href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg', + fetchPriority: 'high', + media: '(max-width: 29.99em)', + imageSizes: '50vw', + imageSrcSet: [240, 480].map((width) => `${src} ${width}w`).join(', ') + }, + { + rel: 'preload', + as: 'image', + href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg', + fetchPriority: 'high', + media: '(min-width: 30em) and (max-width: 47.99em)', + imageSizes: '50vw', + imageSrcSet: [384, 768].map((width) => `${src} ${width}w`).join(', ') + }, + { + rel: 'preload', + as: 'image', + href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg', + fetchPriority: 'high', + media: '(min-width: 48em) and (max-width: 61.99em)', + imageSizes: '20vw', + imageSrcSet: [198, 396].map((width) => `${src} ${width}w`).join(', ') + }, + { + rel: 'preload', + as: 'image', + href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg', + fetchPriority: 'high', + media: '(min-width: 62em) and (max-width: 79.99em)', + imageSizes: '20vw', + imageSrcSet: [256, 512].map((width) => `${src} ${width}w`).join(', ') + }, + { + rel: 'preload', + as: 'image', + href: 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4cd0a798/images/large/PG.10216885.JJ169XX.PZ.jpg', + fetchPriority: 'high', + media: '(min-width: 80em)', + imageSizes: '25vw', + imageSrcSet: [384, 768].map((width) => `${src} ${width}w`).join(', ') + } + ]) }) test.each(['high', 'low', 'auto'])( 'renders an image using an explicit "%s" fetch priority', (fetchPriority) => { - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).toHaveAttribute('fetchpriority', fetchPriority) + expect(wrapper.firstElementChild).toBe(elements[0]) const helmet = Helmet.peek() if (fetchPriority === 'high') { @@ -155,7 +319,8 @@ describe('Dynamic Image Component', () => { expect(helmet.linkTags[0]).toStrictEqual({ as: 'image', href: src, - rel: 'preload' + rel: 'preload', + fetchPriority: 'high' }) } else { expect(helmet.linkTags).toStrictEqual([]) @@ -164,8 +329,9 @@ describe('Dynamic Image Component', () => { ) test('renders an image replacing an invalid fetch priority with the default "auto" value', () => { - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).toHaveAttribute('fetchpriority', 'auto') - expect(Helmet.peek().linkTags).toStrictEqual([]) + expect(wrapper.firstElementChild).toBe(elements[0]) + expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([]) }) - test('renders an explicitly given image component without modifications', () => { - const {getAllByTitle} = renderWithProviders( + test('renders an explicitly given image component', () => { + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) - expect(elements[0]).not.toHaveAttribute('fetchpriority') - expect(Helmet.peek().linkTags).toStrictEqual([]) + expect(elements[0]).toHaveAttribute('fetchpriority', 'high') + expect(wrapper.firstElementChild).toBe(elements[0]) + expect(Helmet.peek().linkTags).toStrictEqual([ + { + as: 'image', + href: src, + rel: 'preload', + fetchPriority: 'high' + } + ]) }) test('renders an image on the client', () => { isServer.mockReturnValue(false) - const {getAllByTitle} = renderWithProviders( + const {getByTestId, getAllByTitle} = renderWithProviders( { }} /> ) + + const wrapper = getByTestId('dynamic-image') const elements = getAllByTitle(imageProps.title) expect(elements).toHaveLength(1) expect(elements[0]).toHaveAttribute('fetchpriority', 'high') - expect(Helmet.peek().linkTags).toStrictEqual([]) + expect(wrapper.firstElementChild).toBe(elements[0]) + expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([]) }) }) }) diff --git a/packages/template-retail-react-app/app/components/image/index.jsx b/packages/template-retail-react-app/app/components/image/index.jsx index ed66e54fef..ce732786b2 100644 --- a/packages/template-retail-react-app/app/components/image/index.jsx +++ b/packages/template-retail-react-app/app/components/image/index.jsx @@ -8,7 +8,10 @@ import React, {useMemo} from 'react' import {Helmet} from 'react-helmet' import PropTypes from 'prop-types' import {Img} from '@salesforce/retail-react-app/app/components/shared/ui' -import {getImageAttributes} from '@salesforce/retail-react-app/app/utils/image' +import { + getImageAttributes, + getImageLinkAttributes +} from '@salesforce/retail-react-app/app/utils/image' import {isServer} from '@salesforce/retail-react-app/app/components/image/utils' /** @@ -24,18 +27,7 @@ const Image = (props) => { const Component = as ? as : Img const [effectiveImageProps, effectiveLinkProps] = useMemo(() => { const imageProps = getImageAttributes(rest) - const loadingStrategy = imageProps?.loading?.toLowerCase?.() - const fetchPriority = imageProps?.fetchPriority?.toLowerCase?.() - const linkProps = - fetchPriority === 'high' && (!loadingStrategy || loadingStrategy === 'eager') - ? { - rel: 'preload', - as: 'image', - href: imageProps.src, - ...(imageProps.sizes ? {imageSizes: imageProps.sizes} : {}), - ...(imageProps.srcSet ? {imageSrcSet: imageProps.srcSet} : {}) - } - : undefined + const linkProps = getImageLinkAttributes(imageProps) return [imageProps, linkProps] }, [rest]) diff --git a/packages/template-retail-react-app/app/components/image/index.test.js b/packages/template-retail-react-app/app/components/image/index.test.js index e2d3fbbd34..5a292698e3 100644 --- a/packages/template-retail-react-app/app/components/image/index.test.js +++ b/packages/template-retail-react-app/app/components/image/index.test.js @@ -83,7 +83,8 @@ describe('Image Component', () => { expect(helmet.linkTags[0]).toStrictEqual({ as: 'image', href: imageProps.src, - rel: 'preload' + rel: 'preload', + fetchPriority: 'high' }) }) @@ -103,7 +104,8 @@ describe('Image Component', () => { expect(helmet.linkTags[0]).toStrictEqual({ as: 'image', href: imageProps.src, - rel: 'preload' + rel: 'preload', + fetchPriority: 'high' }) } else { expect(helmet.linkTags).toStrictEqual([]) @@ -134,7 +136,8 @@ describe('Image Component', () => { expect(helmet.linkTags[0]).toStrictEqual({ as: 'image', href: imageProps.src, - rel: 'preload' + rel: 'preload', + fetchPriority: 'high' }) }) diff --git a/packages/template-retail-react-app/app/components/island/README.md b/packages/template-retail-react-app/app/components/island/README.md index 27ef7fdf00..19c5dcd974 100644 --- a/packages/template-retail-react-app/app/components/island/README.md +++ b/packages/template-retail-react-app/app/components/island/README.md @@ -2,7 +2,7 @@ Inspired by [Astro’s islands architecture](https://docs.astro.build/en/concepts/islands/), this component is intended to give developers explicit and fine-granular control over the hydration behavior of their experiences. -> **ℹ️ Important:** The aspect of **inspiration** is crucial to emphasize. This component merely attempts to **mimic** some of the islands architecture’s behaviors. It needs to be understood as a single tool in a larger toolbox, which is intended to eventually help address very specific performance problems in connection with server-rendered components and their subsequent hydration on the client. So this is neither a silver bullet nor a construct that is guaranteed to work for every use case. +> **ℹ️ Important:** The aspect of **inspiration** is crucial to emphasize. This component merely attempts to **mimic** some of the islands architecture’s behaviors. It needs to be understood as a single tool in a larger toolbox, which is intended to eventually help address very specific performance problems in connection with server-rendered components and their subsequent hydration on the client. So this is neither a silver bullet nor a construct that is guaranteed to work for every use case. Use it wisely and carefully. # 🧟️ Hydration’s Curse @@ -26,17 +26,17 @@ Ultimately, the goal is to reduce the load on the browser’s main thread, which ## React’s Selective Hydration ≠ Partial Hydration -React 18, with [Suspense](https://react.dev/reference/react/Suspense), laid the foundation to officially support concepts such as [selective hydration](https://github.com/reactwg/react-18/discussions/37). However, while the Islands Architecture concept treats partial and selective hydration as interchangeable terms, React’s understanding of selective hydration differs in key aspects. +React 18, with [`Suspense`](https://react.dev/reference/react/Suspense), laid the foundation to officially support concepts such as [selective hydration](https://github.com/reactwg/react-18/discussions/37). However, while the Islands Architecture concept treats partial and selective hydration as interchangeable terms, React’s understanding of selective hydration differs in key aspects. -Suspense _delays the **rendering** of certain components altogether_ and instead enables the display of specified fallback content while the actual content is loading. +`Suspense` _delays the **rendering** of certain components altogether_ and instead enables the display of specified fallback content while the actual content is loading. -When using Suspense, certain contents are therefore not rendered on the server at all, but instead asynchronously on the client at a later time only — with all the associated implications for accessibility and SEO. Depending on the runtime used, the boundaries can be fluid as to when the asynchronous loading of the actual/final content begins — whether already on the server or only on the client — but the outcome remains the same: **Suspense enables asynchronous rendering, not asynchronous hydration.** +When using `Suspense`, certain contents are therefore not rendered on the server at all, but instead asynchronously on the client at a later time only — with all the associated implications for accessibility and SEO. Depending on the runtime used, the boundaries can be fluid as to when the asynchronous loading of the actual/final content begins — whether already on the server or only on the client — but the outcome remains the same: **`Suspense` enables asynchronous rendering, not asynchronous hydration.** -In addition to Suspense, [Server Components](https://react.dev/reference/rsc/server-components) are now another official concept in React for addressing the all-or-nothing problem with hydration. However, as the name suggests, Server Components are components that are rendered exclusively on the server and therefore don’t receive any downstream client-side hydration. +In addition to `Suspense`, [React Server Components](https://react.dev/reference/rsc/server-components) (RSC) are now another powerful official concept in React for addressing the all-or-nothing problem with hydration. Server Components are components that are rendered exclusively on the server and therefore don’t receive any downstream client-side hydration. In fact, when using RSCs, only the JavaScript that is absolutely necessary to restore application state and interactivity is sent to the client. If used sensibly, RSCs in combination with client components - defined via the `use client` directive - enable a very granular control over the sections of an application requiring hydration. However, it’s then still not possible to actively influence the timing of when the hydration of specific sections should occur. ## `` Component -Unlike Suspense, the `` component explicitly **encourages the synchronous rendering of HTML on the server** as much as possible. And instead of delaying the rendering of certain contents, it allows the **explicit delay of the hydration process itself**. +Unlike `Suspense`, the `` component explicitly **encourages the synchronous rendering of HTML on the server** as much as possible. And instead of delaying the rendering of certain contents, it allows the **explicit delay of the hydration process itself**. This delay can be achieved using various `hydrateOn` strategies: @@ -45,7 +45,11 @@ This delay can be achieved using various `hydrateOn` strategies: 3. `hydrateOn={'visible'}`: Useful for lower-priority UI elements that don’t need to be immediately interactive. Hydration occurs once the component has [entered the user’s viewport](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver). As a result, such an element doesn’t get hydrated at all if the user never sees it. 4. `hydrateOn={'off'}`: Useful for non-interactive UI elements or lower-priority UI elements to completely suppress the hydration for. On the surface, this behavior overlaps in certain ways with Server Components. However, there are fundamental differences. While server components don’t contribute any JavaScript to what’s delivered to the client, islands are client-oriented constructs, and thus the JavaScript for a non-hydrated section is still fully transmitted. After all, `hydrateOn` can be dynamically updated at any time in case of using a binding property instead of a hard-coded value. An initial value of `off` therefore opens up the possibilities for completely custom hydration triggers. Furthermore, if Server Components aren’t supported by the runtime used, `hydrateOn={'off'}` offers a simple way to at least reduce the hydration overhead for certain non-interactive areas. -> ❗ Ultimately, the approaches of Suspense, Server Components and the `` component aren’t mutually exclusive; on the contrary, they can perfectly complement each other. +> **🔀️ Important:** Partial hydration support is put behind a feature toggle that can be turned on by setting the constant `PARTIAL_HYDRATION_ENABLED` to `true` (see `@salesforce/retail-react-app/app/constants`). + +> **ℹ️️ Note:** `` components only influence the hydration behavior of server-rendered content. Once an application bootstrapped on the client and returned to SPA mode, any subsequent client-side rendering is not impacted by the `` components anymore. + +> **❗ Important:** Ultimately, the approaches of `Suspense`, Server Components and the `` component aren’t mutually exclusive; on the contrary, they can perfectly complement each other. ## `` Nesting @@ -61,8 +65,9 @@ Therefore, a frequently used approach is to present the user with server-generat If not checked or not even considered during development, this behavior can lead to occasionally subtle issues: -1. Certain configurations can simply miss to make specific sections interactive in time — or at all. In such cases, end users may become frustrated when faced with seemingly interactive content that doesn’t respond to interactions or otherwise behaves unexpectedly. -2. An `` may wrap sections that render different content on the server than on the client. Depending on the timing of hydration or the placement of the `` within the user’s initial viewport, such content mismatches can cause unexpected layout shifts. -3. Managing focus states, keyboard navigation, and screen reader announcements across the boundary between static and interactive content requires careful coordination. +1. An `` might contain unhydrated content that is relevant for side effects or tasks such as metrics tracking. In its unhydrated static state, certain event or beacon submissions may therefore not occur. _Please test the correct functionality of all conditionally/partially hydrated areas carefully._ +2. Certain configurations can simply miss to make specific sections interactive in time — or at all. In such cases, end users may become frustrated when faced with seemingly interactive content that doesn’t respond to interactions or otherwise behaves unexpectedly. +3. An `` may wrap sections that render different content on the server than on the client. Depending on the timing of hydration or the placement of the `` within the user’s initial viewport, such content mismatches can cause unexpected layout shifts. +4. Managing focus states, keyboard navigation, and screen reader announcements across the boundary between static and interactive content requires careful coordination. These challenges aren’t insurmountable, but they require careful architectural planning and testing to address effectively. diff --git a/packages/template-retail-react-app/app/components/island/index.jsx b/packages/template-retail-react-app/app/components/island/index.jsx index 739b6d9d4f..f027649c4a 100644 --- a/packages/template-retail-react-app/app/components/island/index.jsx +++ b/packages/template-retail-react-app/app/components/island/index.jsx @@ -4,7 +4,8 @@ * 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 */ -/*global globalThis*/ +/* eslint-disable react-hooks/rules-of-hooks */ +/* global globalThis */ import React, { Children, createContext, @@ -17,6 +18,7 @@ import React, { } from 'react' import PropTypes from 'prop-types' import {isServer} from '@salesforce/retail-react-app/app/components/island/utils' +import {PARTIAL_HYDRATION_ENABLED} from '@salesforce/retail-react-app/app/constants' const IslandContext = createContext(null) @@ -40,7 +42,9 @@ function findChildren(children, componentType) { /** * This component is intended to give developers explicit and fine-granular control over the - * hydration behavior of their experiences. + * hydration behavior of their experiences. The influence of the `` components on the + * hydration behavior can be activated or deactivated using the {@link PARTIAL_HYDRATION_ENABLED} + * constant. * @param {Object} props * @param {ReactNode} [props.children] The child tree * @param {('load' | 'idle' | 'visible' | 'off')} [props.hydrateOn='load'] The island's hydration strategy @@ -79,7 +83,12 @@ function findChildren(children, componentType) { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver} */ function Island(props) { - const {children, hydrateOn = 'load', options, clientOnly = false} = props + const {children} = props + if (!PARTIAL_HYDRATION_ENABLED) { + return <>{children} + } + + const {hydrateOn = 'load', options, clientOnly = false} = props const ssr = isServer() const [hydrated, setHydrated] = useState(ssr) // Ensure SSR immediately returns the generated HTML const context = useIslandContext() @@ -90,7 +99,6 @@ function Island(props) { } if (!ssr) { - // eslint-disable-next-line react-hooks/rules-of-hooks useLayoutEffect(() => { if ( !hydrated && @@ -105,7 +113,6 @@ function Island(props) { } }) - // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { if ( hydrated || diff --git a/packages/template-retail-react-app/app/components/island/index.test.js b/packages/template-retail-react-app/app/components/island/index.test.js index 89e04ea5f9..c76fd5ee25 100644 --- a/packages/template-retail-react-app/app/components/island/index.test.js +++ b/packages/template-retail-react-app/app/components/island/index.test.js @@ -4,11 +4,13 @@ * 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 */ +/* eslint-disable no-import-assign */ import React from 'react' import {act, render, screen} from '@testing-library/react' import {renderToString} from 'react-dom/server' import Island from '@salesforce/retail-react-app/app/components/island' import {isServer} from '@salesforce/retail-react-app/app/components/island/utils' +import * as constants from '@salesforce/retail-react-app/app/constants' jest.mock('@salesforce/retail-react-app/app/components/island/utils', () => ({ ...jest.requireActual('@salesforce/retail-react-app/app/components/island/utils'), @@ -44,8 +46,15 @@ function renderServerComponent(component) { } describe('Island Component', () => { + let originalFlagValue + + beforeAll(() => (originalFlagValue = constants.PARTIAL_HYDRATION_ENABLED)) + + afterAll(() => Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', originalFlagValue)) + beforeEach(() => { jest.clearAllMocks() + Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', true) global.requestIdleCallback = mockRequestIdleCallback global.cancelIdleCallback = mockCancelIdleCallback global.IntersectionObserver = mockIntersectionObserver @@ -61,6 +70,19 @@ describe('Island Component', () => { isServer.mockReturnValue(true) }) + test('should not render an island at all if constant "PARTIAL_HYDRATION_ENABLED" is false', () => { + Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', false) + + const {container} = render( + +
Server Content
+
+ ) + expect(screen.getByTestId('server-content')).toBeInTheDocument() + expect(screen.getByText('Server Content')).toBeInTheDocument() + expect(screen.getByTestId('server-content')).toBe(container.firstElementChild) + }) + test('should render children immediately', () => { const {container} = render( @@ -98,6 +120,19 @@ describe('Island Component', () => { }) describe('Client-Side Rendering (CSR)', () => { + test('should not render an island at all if constant "PARTIAL_HYDRATION_ENABLED" is false', () => { + Reflect.set(constants, 'PARTIAL_HYDRATION_ENABLED', false) + + const {container} = render( + +
Server Content
+
+ ) + expect(screen.getByTestId('server-content')).toBeInTheDocument() + expect(screen.getByText('Server Content')).toBeInTheDocument() + expect(screen.getByTestId('server-content')).toBe(container.firstElementChild) + }) + test('should hydrate immediately if no SSR content exists', () => { const {container} = renderServerComponent() diff --git a/packages/template-retail-react-app/app/components/passwordless-login/index.jsx b/packages/template-retail-react-app/app/components/passwordless-login/index.jsx index 642a33135d..123c94b78a 100644 --- a/packages/template-retail-react-app/app/components/passwordless-login/index.jsx +++ b/packages/template-retail-react-app/app/components/passwordless-login/index.jsx @@ -12,20 +12,19 @@ import {Button, Divider, Stack, Text} from '@salesforce/retail-react-app/app/com import LoginFields from '@salesforce/retail-react-app/app/components/forms/login-fields' import StandardLogin from '@salesforce/retail-react-app/app/components/standard-login' import SocialLogin from '@salesforce/retail-react-app/app/components/social-login' -import {LOGIN_TYPES} from '@salesforce/retail-react-app/app/constants' +const noop = () => {} const PasswordlessLogin = ({ form, handleForgotPasswordClick, handlePasswordlessLoginClick, isSocialEnabled = false, idps = [], - setLoginType + setLoginType = noop }) => { const [showPasswordView, setShowPasswordView] = useState(false) const handlePasswordButton = async (e) => { - setLoginType(LOGIN_TYPES.PASSWORD) const isValid = await form.trigger() // Manually trigger the browser native form validations const domForm = e.target.closest('form') @@ -48,8 +47,8 @@ const PasswordlessLogin = ({ /> )} - {/* Only show edit button if it's not a standard product */} - {variant.id && - !variant.type?.item && ( // the variant.id ensures complete product data. Without it, Edit button appears briefly - - )} + {variant.id && !variant.type?.item && !isBonusProduct && ( + + )} {!isBonusProduct && ( diff --git a/packages/template-retail-react-app/app/pages/cart/partials/cart-secondary-button-group.test.js b/packages/template-retail-react-app/app/pages/cart/partials/cart-secondary-button-group.test.js index 0f041f3a9a..39decad94e 100644 --- a/packages/template-retail-react-app/app/pages/cart/partials/cart-secondary-button-group.test.js +++ b/packages/template-retail-react-app/app/pages/cart/partials/cart-secondary-button-group.test.js @@ -170,11 +170,10 @@ describe('CartSecondaryButtonGroup Edit button conditional rendering', () => { }) }) -test('hides remove, wishlist and gift checkbox for bonus product', async () => { +test('hides remove, wishlist, edit button and gift checkbox for bonus product', async () => { const {user} = renderWithProviders() - expect(screen.getByRole('button', {name: /edit/i})).toBeInTheDocument() - + expect(screen.queryByRole('button', {name: /edit/i})).not.toBeInTheDocument() expect(screen.queryByRole('button', {name: /remove/i})).not.toBeInTheDocument() expect(screen.queryByRole('button', {name: /add to wishlist/i})).not.toBeInTheDocument() expect(screen.queryByRole('checkbox', {name: /this is a gift/i})).not.toBeInTheDocument() diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx b/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx index d47b91a734..cf98217cef 100644 --- a/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx +++ b/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx @@ -80,7 +80,6 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id const [authModalView, setAuthModalView] = useState(PASSWORD_VIEW) const authModal = useAuthModal(authModalView) - const [isPasswordlessLoginClicked, setIsPasswordlessLoginClicked] = useState(false) const passwordlessConfigCallback = getConfig().app.login?.passwordless?.callbackURI const callbackURL = isAbsoluteURL(passwordlessConfigCallback) ? passwordlessConfigCallback @@ -107,11 +106,6 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id const submitForm = async (data) => { setError(null) - if (isPasswordlessLoginClicked) { - handlePasswordlessLogin(data.email) - setIsPasswordlessLoginClicked(false) - return - } try { if (!data.password) { await updateCustomerForBasket.mutateAsync({ @@ -166,8 +160,15 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id } }, [showPasswordField]) - const onPasswordlessLoginClick = async () => { - setIsPasswordlessLoginClicked(true) + const onPasswordlessLoginClick = async (e) => { + const isValid = await form.trigger('email') + const domForm = e.target.closest('form') + if (isValid && domForm.checkValidity()) { + const email = form.getValues().email + await handlePasswordlessLogin(email) + } else { + domForm.reportValidity() + } } return ( diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.test.js b/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.test.js index 34333b73b5..cc42fcff79 100644 --- a/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.test.js +++ b/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.test.js @@ -48,6 +48,7 @@ jest.mock('../util/checkout-context', () => { afterEach(() => { jest.resetModules() + jest.restoreAllMocks() }) describe('passwordless and social disabled', () => { @@ -149,8 +150,10 @@ describe('passwordless enabled', () => { test('allows passwordless login', async () => { jest.spyOn(window, 'location', 'get').mockReturnValue({ - pathname: '/checkout' + pathname: '/checkout', + origin: 'https://example.com' }) + const {user} = renderWithProviders() // enter a valid email address @@ -158,8 +161,6 @@ describe('passwordless enabled', () => { // initiate passwordless login const passwordlessLoginButton = screen.getByText('Secure Link') - // Click the button twice as the isPasswordlessLoginClicked state doesn't change after the first click - await user.click(passwordlessLoginButton) await user.click(passwordlessLoginButton) expect( mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync @@ -177,7 +178,7 @@ describe('passwordless enabled', () => { }) // resend the email - user.click(screen.getByText(/Resend Link/i)) + await user.click(screen.getByText(/Resend Link/i)) expect( mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync ).toHaveBeenCalledWith({ @@ -241,6 +242,42 @@ describe('passwordless enabled', () => { }) } ) + + test('allows guest checkout via Enter key', async () => { + const {user} = renderWithProviders() + + // enter a valid email address + await user.type(screen.getByLabelText('Email'), validEmail) + + // submit via Enter key - should trigger guest checkout + await user.keyboard('{Enter}') + + // should update customer info for basket (guest checkout) + await waitFor(() => { + expect(currentBasket.customerInfo.email).toBe(validEmail) + }) + }) + + test('allows login via Enter key when password is provided', async () => { + const {user} = renderWithProviders() + + // enter a valid email address + await user.type(screen.getByLabelText('Email'), validEmail) + + // switch to password mode + const passwordButton = screen.getByText('Password') + await user.click(passwordButton) + + // enter password + await user.type(screen.getByLabelText('Password'), password) + + // submit via Enter key - should trigger login + await user.keyboard('{Enter}') + + expect( + mockAuthHelperFunctions[AuthHelpers.LoginRegisteredUserB2C].mutateAsync + ).toHaveBeenCalledWith({username: validEmail, password: password}) + }) }) describe('social login enabled', () => { diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/login-state.jsx b/packages/template-retail-react-app/app/pages/checkout/partials/login-state.jsx index 24af933e7d..85329de57f 100644 --- a/packages/template-retail-react-app/app/pages/checkout/partials/login-state.jsx +++ b/packages/template-retail-react-app/app/pages/checkout/partials/login-state.jsx @@ -37,9 +37,9 @@ const LoginState = ({