diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 13da93b2c5..8ab0d22710 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -11,18 +11,14 @@ jobs: fail-fast: false matrix: # Run all matrix env at once because we will not deploy demo app to MRT. - node: [18, 20, 22] - npm: [8, 9, 10, 11] - exclude: # node 18 with npm 11 is not compatible - - node: 18 - npm: 11 + node: [20, 22] + npm: [10, 11] runs-on: ubuntu-latest env: # The "default" npm is the one that ships with a given version of node. # For more: https://nodejs.org/en/download/releases/ # (We also use this env var for making sure a step runs once for the current node version) - # Note: For node 18, the default was npm 9 until v18.19.0, when it became npm 10 - IS_DEFAULT_NPM: ${{ (matrix.node == 18 && matrix.npm == 10) || (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} + IS_DEFAULT_NPM: ${{ (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} steps: - name: Checkout uses: actions/checkout@v4 @@ -67,18 +63,14 @@ jobs: max-parallel: 1 matrix: # Run all matrix env at once because we will not deploy demo app to MRT. - node: [18, 20, 22] - npm: [8, 9, 10, 11] - exclude: # node 18 with npm 11 is not compatible - - node: 18 - npm: 11 + node: [20, 22] + npm: [10, 11] runs-on: ubuntu-latest env: # The "default" npm is the one that ships with a given version of node. # For more: https://nodejs.org/en/download/releases/ # (We also use this env var for making sure a step runs once for the current node version) - # Note: For node 18, the default was npm 9 until v18.19.0, when it became npm 10 - IS_DEFAULT_NPM: ${{ (matrix.node == 18 && matrix.npm == 10) || (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} + IS_DEFAULT_NPM: ${{ (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} steps: - name: Checkout @@ -183,18 +175,14 @@ jobs: max-parallel: 1 matrix: # Run all matrix env at once because we will not deploy demo app to MRT. - node: [18, 20, 22] - npm: [8, 9, 10, 11] - exclude: # node 18 with npm 11 is not compatible - - node: 18 - npm: 11 + node: [20, 22] + npm: [10, 11] runs-on: ubuntu-latest env: # The "default" npm is the one that ships with a given version of node. # For more: https://nodejs.org/en/download/releases/ # (We also use this env var for making sure a step runs once for the current node version) - # Note: For node 18, the default was npm 9 until v18.19.0, when it became npm 10 - IS_DEFAULT_NPM: ${{ (matrix.node == 18 && matrix.npm == 10) || (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} + IS_DEFAULT_NPM: ${{ (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} # The current recommended version for Managed Runtime: # https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/upgrade-node-version.html IS_MRT_NODE: ${{ matrix.node == 22 && matrix.npm == 11 }} @@ -303,18 +291,14 @@ jobs: max-parallel: 1 matrix: # Run all matrix env at once because we will not deploy demo app to MRT. - node: [18, 20, 22] - npm: [8, 9, 10, 11] - exclude: # node 18 with npm 11 is not compatible - - node: 18 - npm: 11 + node: [20, 22] + npm: [10, 11] runs-on: ubuntu-latest env: # The "default" npm is the one that ships with a given version of node. # For more: https://nodejs.org/en/download/releases/ # (We also use this env var for making sure a step runs once for the current node version) - # Note: For node 18, the default was npm 9 until v18.19.0, when it became npm 10 - IS_DEFAULT_NPM: ${{ (matrix.node == 18 && matrix.npm == 10) || (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} + IS_DEFAULT_NPM: ${{ (matrix.node == 20 && matrix.npm == 10) || (matrix.node == 22 && matrix.npm == 10) }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-0.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-0.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-0.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-0.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-1.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-1.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-1.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-1.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-3.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-3.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-3.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-3.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/guest/checkout-a11y-violations-step-4-order-confirmation.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-0.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-0.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-0.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-0.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-1.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-1.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-1.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-1.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-2.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-2.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-2.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-2.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-3.json b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-3.json index 29229a07fb..381540a76d 100644 --- a/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-3.json +++ b/e2e/tests/a11y/desktop/slas-private-client/__snapshots__/registered/checkout-a11y-violations-step-3.json @@ -15,22 +15,6 @@ } ] }, - { - "id": "page-has-heading-one", - "impact": "moderate", - "description": "Ensure that the page, or at least one of its frames contains a level-one heading", - "help": "Page should contain a level-one heading", - "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/page-has-heading-one?application=playwright", - "nodes": [ - { - "html": "", - "failureSummary": "Fix all of the following:\n Page must have a level-one heading", - "target": [ - "html" - ] - } - ] - }, { "id": "region", "impact": "moderate", diff --git a/e2e/tests/mobile/dnt.spec.js b/e2e/tests/mobile/dnt.spec.js index 2c8b4e86a3..cbc8d271fb 100644 --- a/e2e/tests/mobile/dnt.spec.js +++ b/e2e/tests/mobile/dnt.spec.js @@ -73,7 +73,7 @@ test('Shopper can use the consent tracking form', async ({page}) => { await checkDntCookie(page, '1') // Logging out clears the preference - await page.getByRole('heading', {name: /My Account/i}).click() + await page.getByRole('button', {name: /My Account chevron-down/i}).click() const buttons = await page.getByText(/Log Out/i).elementHandles() for (const button of buttons) { if (await button.isVisible()) { diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index 7787081935..2304c7b826 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -1,10 +1,11 @@ ## v4.2.0-dev (Sep 26, 2025) +- Prevent headers from being overriden in `generateCustomEndpointOptions` [#3405](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3405/) ## v4.1.0 (Sep 25, 2025) ## v4.0.0 (Sep 04, 2025) -- [Breaking] Upgrade to commerce-sdk-isomorphic v4.0.0 [2879](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2879) +- [Breaking] Upgrade to commerce-sdk-isomorphic v4.0.0 [#2879](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2879) - [Breaking] Remove deprecated properties from useDNT in commerce-sdk-react [#3177](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3177) - Add support for environment level base paths on /mobify routes [#2892](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2892) - Update USID expiry to match SLAS refresh token expiry [#2854](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2854) diff --git a/packages/commerce-sdk-react/src/hooks/helpers.ts b/packages/commerce-sdk-react/src/hooks/helpers.ts index 8aff3a4dc0..84754f9b50 100644 --- a/packages/commerce-sdk-react/src/hooks/helpers.ts +++ b/packages/commerce-sdk-react/src/hooks/helpers.ts @@ -53,6 +53,7 @@ export const generateCustomEndpointOptions = ( return { ...options, options: { + ...options.options, method: options.options?.method || 'GET', headers: { Authorization: `Bearer ${access_token}`, @@ -61,8 +62,7 @@ export const generateCustomEndpointOptions = ( ...globalHeaders, ...options.options?.headers, ...(args?.headers ? args.headers : {}) - }, - ...options.options + } }, clientConfig: { ...globalClientConfig, diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index fc73cc1517..a03918a245 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,4 +1,5 @@ ## v8.2.0-dev (Sep 26, 2025) +- [Bugfix] Fix footer heading semantic consistency and alignment. Fix accessibility compliance by adding proper h1 headings to checkout pages to resolve the page-has-heading-one accessibility rule violation. [#3398](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3398) - [Bugfix] Use `serverSafeEncode` util for address mutations. [#3380](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3380) ## v8.1.0 (Sep 25, 2025) - Updated search UX - prices, images, suggestions new layout [#3271](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3271) @@ -10,6 +11,7 @@ - Only show option to deliver to multiple addresses if there are multiple items in the basket. [#3336](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3336) - Added support for Choice of Bonus Products feature. Users can now select from available bonus products when they qualify for the associated promotion. The bonus product selection flow can be entered from either the "Item Added to Cart" modal (when adding the qualifying product to the cart) or from the cart page. [#3292] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3292) - Add @h4ad/serverless-adapter to jest config [#3325](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3325) +- Fix bug where pick up items were displaying delivery stock levels instead of in store stock levels [#3401](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3401) ## v8.0.0 (Sep 04, 2025) - Add support for environment level base paths on /mobify routes [#2892](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2892) diff --git a/packages/template-retail-react-app/app/components/footer/index.jsx b/packages/template-retail-react-app/app/components/footer/index.jsx index 5b28c2ee4d..9500aad913 100644 --- a/packages/template-retail-react-app/app/components/footer/index.jsx +++ b/packages/template-retail-react-app/app/components/footer/index.jsx @@ -207,7 +207,7 @@ const Subscribe = ({...otherProps}) => { const intl = useIntl() return ( - + {intl.formatMessage({ id: 'footer.subscribe.heading.first_to_know', defaultMessage: 'Be the first to know' diff --git a/packages/template-retail-react-app/app/components/product-item-list/index.jsx b/packages/template-retail-react-app/app/components/product-item-list/index.jsx index f400012577..52df15fe58 100644 --- a/packages/template-retail-react-app/app/components/product-item-list/index.jsx +++ b/packages/template-retail-react-app/app/components/product-item-list/index.jsx @@ -33,7 +33,9 @@ const ProductItemList = ({ removingItemIds = [], // Styling options hideBorder = false, - hideBottomBorder = false + hideBottomBorder = false, + // Pickup information + getShipmentInfoForProduct = null }) => { return ( @@ -42,11 +44,18 @@ const ProductItemList = ({ // Check if this product item (regular or bonus) is being removed const isBeingRemoved = removingItemIds.includes(productItem.itemId) + // Get pickup information for this product item + const shipmentInfo = getShipmentInfoForProduct + ? getShipmentInfoForProduct(productItem) + : null + const pickupInStore = shipmentInfo?.isPickupOrder || false + return ( { const {stepQuantity, showInventoryMessage, inventoryMessage, quantity, setQuantity} = - useDerivedProduct(product) + useDerivedProduct(product, false, false, pickupInStore) const {currency: activeCurrency} = useCurrency() return ( { return getPriceData(product, {quantity}) }, [product, quantity]) diff --git a/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js b/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js index d52203fe83..0bdf40b18b 100644 --- a/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js +++ b/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js @@ -427,7 +427,7 @@ test('Allows customer to create an account', async () => { () => { expect(window.location.pathname).toBe('/uk/en-GB/account') const myAccount = screen.getAllByText(/My Account/) - expect(myAccount).toHaveLength(2) + expect(myAccount).toHaveLength(3) // h1 (sr-only), h2 (accordion), h2 (sidebar) }, { timeout: 5000 diff --git a/packages/template-retail-react-app/app/hooks/use-derived-product.js b/packages/template-retail-react-app/app/hooks/use-derived-product.js index 1fad725d47..b7b5d5d1e7 100644 --- a/packages/template-retail-react-app/app/hooks/use-derived-product.js +++ b/packages/template-retail-react-app/app/hooks/use-derived-product.js @@ -23,10 +23,12 @@ const getInventoryById = (product, inventoryId) => { } // TODO: This needs to be refactored. +// If compatibility with API version < v8.1 is needed, keep pickupInStore as false export const useDerivedProduct = ( product, isProductPartOfSet = false, - isProductPartOfBundle = false + isProductPartOfBundle = false, + pickupInStore = false ) => { const showLoading = !product const isProductABundle = product?.type?.bundle @@ -49,14 +51,6 @@ export const useDerivedProduct = ( const [quantity, setQuantity] = useState(initialQuantity) const {selectedStore} = useSelectedStore() - const selectedStoreInventory = getInventoryById(product, selectedStore?.inventoryId) - const selectedStoreStockLevel = selectedStoreInventory?.stockLevel || 0 - // selectedStoreStockLevel and selectedStoreInventory are already variant specific, - // so we don't need to check for variation attributes - const isSelectedStoreOutOfStock = - !selectedStoreStockLevel || - selectedStoreStockLevel < quantity || - !selectedStoreInventory?.orderable // A product is considered out of stock if the stock level is 0 or if we have all our // variation attributes selected, but don't have a variant. We do this because the API // will sometimes return all the variants even if they are out of stock, but for other @@ -69,34 +63,54 @@ export const useDerivedProduct = ( Object.keys(variationParams).length === variationAttributes.length) || (!isProductABundle && variant && !variant.orderable) const unfulfillable = stockLevel < quantity + + // Product details for selected store + const selectedStoreInventory = getInventoryById(product, selectedStore?.inventoryId) + const selectedStoreStockLevel = selectedStoreInventory?.stockLevel || 0 + const selectedStoreLowestStockLevelProductName = + selectedStoreInventory?.lowestStockLevelProductName + // selectedStoreStockLevel and selectedStoreInventory are already variant specific, + // so we don't need to check for variation attributes + const isSelectedStoreOutOfStock = !selectedStoreStockLevel || !selectedStoreInventory?.orderable + const selectedStoreUnfulfillable = selectedStoreStockLevel < quantity + + // Use appropriate inventory based on pickup/delivery selection + const currentStockLevel = pickupInStore ? selectedStoreStockLevel : stockLevel + const currentLowestStockLevelProductName = pickupInStore + ? selectedStoreLowestStockLevelProductName + : lowestStockLevelProductName + const currentIsOutOfStock = pickupInStore ? isSelectedStoreOutOfStock : isOutOfStock + const currentUnfulfillable = pickupInStore ? selectedStoreUnfulfillable : unfulfillable + const inventoryMessages = { [OUT_OF_STOCK]: intl.formatMessage({ defaultMessage: 'Out of stock', id: 'use_product.message.out_of_stock' }), - [UNFULFILLABLE]: lowestStockLevelProductName + [UNFULFILLABLE]: currentLowestStockLevelProductName ? intl.formatMessage( { defaultMessage: 'Only {stockLevel} left for {productName}!', id: 'use_product.message.inventory_remaining_for_product' }, - {stockLevel, productName: lowestStockLevelProductName} + {stockLevel: currentStockLevel, productName: currentLowestStockLevelProductName} ) : intl.formatMessage( { defaultMessage: 'Only {stockLevel} left!', id: 'use_product.message.inventory_remaining' }, - {stockLevel} + {stockLevel: currentStockLevel} ) } // showInventoryMessage controls if add to cart button is disabled const showInventoryMessage = - (variant || isProductABundle || isStandardProduct) && (isOutOfStock || unfulfillable) + (variant || isProductABundle || isStandardProduct) && + (currentIsOutOfStock || currentUnfulfillable) const inventoryMessage = - (isOutOfStock && inventoryMessages[OUT_OF_STOCK]) || - (unfulfillable && inventoryMessages[UNFULFILLABLE]) + (currentIsOutOfStock && inventoryMessages[OUT_OF_STOCK]) || + (currentUnfulfillable && inventoryMessages[UNFULFILLABLE]) // If the `initialQuantity` changes, update the state. This typically happens // when either the master product changes, or the inventory of the product changes @@ -105,6 +119,7 @@ export const useDerivedProduct = ( setQuantity(initialQuantity) }, [initialQuantity]) + // Lump the out of stock/unfulfillable checks together for easier consumption return { showLoading, showInventoryMessage, @@ -119,7 +134,7 @@ export const useDerivedProduct = ( stockLevel, isOutOfStock, unfulfillable, - isSelectedStoreOutOfStock, + isSelectedStoreOutOfStock: isSelectedStoreOutOfStock || selectedStoreUnfulfillable, selectedStore } } diff --git a/packages/template-retail-react-app/app/hooks/use-derived-product.test.js b/packages/template-retail-react-app/app/hooks/use-derived-product.test.js index 5e4f6c8726..7264eaf430 100644 --- a/packages/template-retail-react-app/app/hooks/use-derived-product.test.js +++ b/packages/template-retail-react-app/app/hooks/use-derived-product.test.js @@ -34,7 +34,7 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({ }) })) -const MockComponent = ({product}) => { +const MockComponent = ({product, pickupInStore = false}) => { const { inventoryMessage, showInventoryMessage, @@ -43,7 +43,7 @@ const MockComponent = ({product}) => { variant, isSelectedStoreOutOfStock, selectedStore - } = useDerivedProduct(product) + } = useDerivedProduct(product, false, false, pickupInStore) return (
@@ -60,7 +60,8 @@ const MockComponent = ({product}) => { } MockComponent.propTypes = { - product: PropTypes.object + product: PropTypes.object, + pickupInStore: PropTypes.bool } describe('useDerivedProduct hook', () => { @@ -353,5 +354,100 @@ describe('useDerivedProduct hook', () => { screen.getByText(/{"inventoryId":"inventory_m_store_store1"}/) ).toBeInTheDocument() }) + + test('when pickupInStore is true and store has no stock, should show out of stock message', () => { + // Mock useSelectedStore to return a store with inventoryId + useSelectedStore.mockReturnValue({ + selectedStore: {inventoryId}, + isLoading: false, + error: null, + hasSelectedStore: true + }) + + // Mock useVariant to return a valid variant + useVariant.mockReturnValue({ + orderable: true, + price: 299.99, + productId: '750518699578M', + variationValues: {color: 'BLACKFB', size: '038', width: 'V'} + }) + + const mockData = { + ...mockProductDetail, + quantity: 10, + // Default inventory has plenty of stock + inventory: { + ats: 50, + backorderable: false, + id: 'inventory_m', + orderable: true, + preorderable: false, + stockLevel: 50 + }, + // Store inventory has no stock + inventories: [ + { + ats: 5, + backorderable: false, + id: 'inventory_m_store_store1', + orderable: true, + preorderable: false, + stockLevel: 0 + } + ] + } + + renderWithProviders() + + // Should show out of stock message based on store inventory stock level being 0 + expect(screen.getByText(/Out of stock/)).toBeInTheDocument() + expect(screen.getByText(/isStoreOutOfStock: true/)).toBeInTheDocument() + }) + + test('when pickupInStore is true and store has limited stock, should show unfulfillable message', () => { + // Mock useSelectedStore to return a store with inventoryId + useSelectedStore.mockReturnValue({ + selectedStore: {inventoryId}, + isLoading: false, + error: null, + hasSelectedStore: true + }) + + useVariant.mockReturnValue(null) + + const mockData = { + ...mockProductDetail, + type: { + bundle: true + }, + quantity: 10, + // Default inventory has plenty of stock + inventory: { + ats: 50, + backorderable: false, + id: 'inventory_m', + orderable: true, + preorderable: false, + stockLevel: 50 + }, + // Store inventory has limited stock but is orderable + inventories: [ + { + ats: 5, + backorderable: false, + id: 'inventory_m_store_store1', + orderable: true, + preorderable: false, + stockLevel: 5 + } + ] + } + + renderWithProviders() + + // Limited stock shows unfulfillable message + expect(screen.getByText(/Only 5 left!/)).toBeInTheDocument() + expect(screen.getByText(/isStoreOutOfStock: true/)).toBeInTheDocument() + }) }) }) diff --git a/packages/template-retail-react-app/app/pages/account/index.jsx b/packages/template-retail-react-app/app/pages/account/index.jsx index 494b472e66..51a2587aa8 100644 --- a/packages/template-retail-react-app/app/pages/account/index.jsx +++ b/packages/template-retail-react-app/app/pages/account/index.jsx @@ -124,6 +124,9 @@ const Account = () => { layerStyle="page" paddingTop={[4, 4, 12, 12, 16]} > + + + {/* small screen nav accordion */} diff --git a/packages/template-retail-react-app/app/pages/cart/index.jsx b/packages/template-retail-react-app/app/pages/cart/index.jsx index 7c4c802bf1..19f6210839 100644 --- a/packages/template-retail-react-app/app/pages/cart/index.jsx +++ b/packages/template-retail-react-app/app/pages/cart/index.jsx @@ -15,7 +15,8 @@ import { Grid, GridItem, Container, - useDisclosure + useDisclosure, + Heading } from '@salesforce/retail-react-app/app/components/shared/ui' // Project Components @@ -997,6 +998,9 @@ const Cart = () => { return ( + + + { renderSecondaryActions={ renderSecondaryActions } + getShipmentInfoForProduct={ + getShipmentInfoForProduct + } renderDeliveryActions={( productItem ) => { @@ -1135,6 +1142,9 @@ const Cart = () => { renderSecondaryActions={ renderSecondaryActions } + getShipmentInfoForProduct={ + getShipmentInfoForProduct + } renderDeliveryActions={( productItem ) => @@ -1174,6 +1184,9 @@ const Cart = () => { renderSecondaryActions={ renderSecondaryActions } + getShipmentInfoForProduct={ + getShipmentInfoForProduct + } renderDeliveryActions={( productItem ) => { diff --git a/packages/template-retail-react-app/app/pages/checkout/confirmation.jsx b/packages/template-retail-react-app/app/pages/checkout/confirmation.jsx index 2940f1b486..6230aeebfb 100644 --- a/packages/template-retail-react-app/app/pages/checkout/confirmation.jsx +++ b/packages/template-retail-react-app/app/pages/checkout/confirmation.jsx @@ -173,7 +173,7 @@ const CheckoutConfirmation = () => { - + { return ( + + + { return ( + + + { return ( + + + { expect(window.location.pathname).toBe('/uk/en-GB/account') expect(screen.getByText(/My Profile/i)).toBeInTheDocument() diff --git a/packages/template-retail-react-app/app/pages/page-not-found/index.jsx b/packages/template-retail-react-app/app/pages/page-not-found/index.jsx index 06cf676e78..9a2c74cc74 100644 --- a/packages/template-retail-react-app/app/pages/page-not-found/index.jsx +++ b/packages/template-retail-react-app/app/pages/page-not-found/index.jsx @@ -15,7 +15,7 @@ import { Text } from '@salesforce/retail-react-app/app/components/shared/ui' import {Helmet} from 'react-helmet' -import {useIntl} from 'react-intl' +import {useIntl, FormattedMessage} from 'react-intl' import {useServerContext} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks' import {SearchIcon} from '@salesforce/retail-react-app/app/components/icons' import {useHistory} from 'react-router-dom' @@ -37,6 +37,12 @@ const PageNotFound = () => { height={'100%'} padding={{lg: 8, md: 6, sm: 0, base: 0}} > + + + {intl.formatMessage({ diff --git a/packages/template-retail-react-app/app/pages/product-detail/index.jsx b/packages/template-retail-react-app/app/pages/product-detail/index.jsx index 1245662660..de950d8a0f 100644 --- a/packages/template-retail-react-app/app/pages/product-detail/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-detail/index.jsx @@ -12,7 +12,7 @@ import {FormattedMessage, useIntl} from 'react-intl' import {getUpdateBundleChildArray} from '@salesforce/retail-react-app/app/utils/product-utils' // Components -import {Box, Button, Stack} from '@salesforce/retail-react-app/app/components/shared/ui' +import {Box, Button, Stack, Heading} from '@salesforce/retail-react-app/app/components/shared/ui' import { useProduct, useProducts, @@ -649,6 +649,12 @@ const ProductDetail = () => { layerStyle="page" data-testid="product-details-page" > + <Heading as="h1" srOnly> + <FormattedMessage + defaultMessage="Product Details" + id="product_detail.title.product_details" + /> + </Heading> <Helmet> <title>{product?.pageTitle} {product?.pageMetaTags?.length > 0 && diff --git a/packages/template-retail-react-app/app/pages/product-list/partials/page-header.jsx b/packages/template-retail-react-app/app/pages/product-list/partials/page-header.jsx index ee73482521..efdfaa161e 100644 --- a/packages/template-retail-react-app/app/pages/product-list/partials/page-header.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/partials/page-header.jsx @@ -21,10 +21,10 @@ const PageHeader = ({category, productSearchResult, isLoading, searchQuery, ...o {searchQuery && Search Results for} {/* Category Title */} - + {`${category?.name || searchQuery || ''}`} - + {!isLoading && ({productSearchResult?.total})} diff --git a/packages/template-retail-react-app/app/pages/registration/index.jsx b/packages/template-retail-react-app/app/pages/registration/index.jsx index 0bb43b56c6..fc824574d4 100644 --- a/packages/template-retail-react-app/app/pages/registration/index.jsx +++ b/packages/template-retail-react-app/app/pages/registration/index.jsx @@ -7,8 +7,8 @@ import React, {useEffect} from 'react' import PropTypes from 'prop-types' -import {useIntl} from 'react-intl' -import {Box, Container} from '@salesforce/retail-react-app/app/components/shared/ui' +import {useIntl, FormattedMessage} from 'react-intl' +import {Box, Container, Heading} from '@salesforce/retail-react-app/app/components/shared/ui' import {AuthHelpers, useAuthHelper, useCustomerType} from '@salesforce/commerce-sdk-react' import {useForm} from 'react-hook-form' import {useLocation} from 'react-router-dom' @@ -61,6 +61,12 @@ const Registration = () => { return ( + + + { }) ) - await user.click(withinForm.getByText(/create account/i)) + await user.click(withinForm.getByRole('button', {name: /create account/i})) // wait for success state to appear const myAccount = await screen.findAllByText(/My Account/) await waitFor( () => { - expect(myAccount).toHaveLength(2) + expect(myAccount).toHaveLength(3) // h1 (sr-only), h2 (accordion), h2 (sidebar) }, { timeout: 5000 diff --git a/packages/template-retail-react-app/app/pages/reset-password/index.jsx b/packages/template-retail-react-app/app/pages/reset-password/index.jsx index 633a088951..58579e0393 100644 --- a/packages/template-retail-react-app/app/pages/reset-password/index.jsx +++ b/packages/template-retail-react-app/app/pages/reset-password/index.jsx @@ -6,9 +6,9 @@ */ import React, {useEffect} from 'react' -import {useIntl} from 'react-intl' +import {useIntl, FormattedMessage} from 'react-intl' import PropTypes from 'prop-types' -import {Box, Container} from '@salesforce/retail-react-app/app/components/shared/ui' +import {Box, Container, Heading} from '@salesforce/retail-react-app/app/components/shared/ui' import {useForm} from 'react-hook-form' import Seo from '@salesforce/retail-react-app/app/components/seo' import ResetPasswordForm from '@salesforce/retail-react-app/app/components/reset-password' @@ -55,6 +55,12 @@ const ResetPassword = () => { return ( + + +