Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
23c9db0
Handle SDK Client not initialized
shethj Jun 9, 2025
2c19d6e
Added tests for useResolvedClient
shethj Jun 9, 2025
b938450
Handle missing SDK Client ShopperContext
shethj Jun 9, 2025
f72e1d0
ShopperCustomers - Handle SDK Client no init
shethj Jun 9, 2025
0e5bbac
ShopperExperience - Handle missing SDK Client
shethj Jun 9, 2025
d1c7b0f
ShopperGiftCertificates - Handle missing SDK Client
shethj Jun 9, 2025
b1794a6
ShopperLogin: Handle missing SDK client
shethj Jun 9, 2025
2e59f77
ShopperOrders - Handle Missing SDK Client
shethj Jun 9, 2025
cca81f1
ShopperProducts: Handle missing SDK Client
shethj Jun 9, 2025
ecfd927
ShopperPromotions: Handle missing SDK client
shethj Jun 9, 2025
9dcf72c
ShopperSearch - Handle missing SDK Client
shethj Jun 9, 2025
34bba65
ShopperSEO: Handle missing SDK Client
shethj Jun 9, 2025
3968f53
ShopperStores: Handle missing SDK Client
shethj Jun 9, 2025
cb86ce5
Fix linting errors
shethj Jun 9, 2025
7660fde
Update useResolvedClient tests
shethj Jun 9, 2025
91c6909
Update changelog
shethj Jun 9, 2025
16243f7
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 9, 2025
cdbc939
implemented the voiceover feature for the email confirmation modal on…
larnelleankunda Jun 9, 2025
bec77e6
Update changelog
shethj Jun 9, 2025
83b6f12
Extract CLIENT_KEY literal to constnt
shethj Jun 9, 2025
c88c1ab
updated the chanege log file to point to current changes and saved ch…
larnelleankunda Jun 9, 2025
50ae895
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 9, 2025
4e31416
linted the files and built translations
larnelleankunda Jun 9, 2025
8452493
changed role and aria label tags to solve unsuccessful check issues
larnelleankunda Jun 10, 2025
859b62d
Implemented changes to create more successful checks after facing a t…
larnelleankunda Jun 10, 2025
bc60eeb
Linted my files
larnelleankunda Jun 10, 2025
36dd723
Remove additional hook for client validation
shethj Jun 10, 2025
0273032
Fix linting
shethj Jun 10, 2025
f3b2787
Add comments
shethj Jun 10, 2025
524aa80
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 10, 2025
ba5539e
rename apiClients variable to add clarity
shethj Jun 11, 2025
b9f0039
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 11, 2025
1b53054
update variable name for apiClients
shethj Jun 11, 2025
f4a9b42
Merge branch 'develop' into W-17599234-A11Y-email-modal-8
yunakim714 Jun 11, 2025
eaa424d
linted more files to fix unsuccessful check error
larnelleankunda Jun 11, 2025
1876c1b
linted more files to fix unsuccessful check error
larnelleankunda Jun 11, 2025
647808f
linted more files to fix unsuccessful check error
larnelleankunda Jun 11, 2025
9c1303c
Update packages/template-retail-react-app/CHANGELOG.md
larnelleankunda Jun 11, 2025
3a6fd15
taking off the visible focus outline around the modal border
larnelleankunda Jun 11, 2025
0561ab4
taking off the visible focus outline around the modal border
larnelleankunda Jun 11, 2025
f886b5a
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 11, 2025
109915a
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 12, 2025
2c27622
Initial Commit
bendvc Jun 12, 2025
f3621eb
Update CHANGELOG.md
bendvc Jun 12, 2025
88769d2
Update CHANGELOG.md
bendvc Jun 12, 2025
24b08af
Add some additional tests
bendvc Jun 12, 2025
c5c8506
PR Feedback
bendvc Jun 16, 2025
181c8d3
Merge branch 'develop' into bendvc/W-18647820_fix-einstein-add-to-cart
bendvc Jun 16, 2025
71f5843
adding an a11y tag to changes made docuemented in the change log read…
larnelleankunda Jun 16, 2025
389c9c6
Merge branch 'develop' into W-17599234-A11Y-email-modal-8
larnelleankunda Jun 16, 2025
804f2ff
Fix bar reference to master.variantId
bendvc Jun 16, 2025
91bae5a
removed redundant aria label that repeating email confirmation title
larnelleankunda Jun 16, 2025
be7626f
removed redundant aria label that repeating email confirmation title
larnelleankunda Jun 16, 2025
f3b718e
mitigating the need for additional translations
larnelleankunda Jun 16, 2025
9969319
feat: cursor rules for unit tests
szirpesf Jun 17, 2025
ccf10b3
Merge pull request #2577 from SalesforceCommerceCloud/sz-W-18677509-c…
szirpesf Jun 17, 2025
0e5fc4b
Merge branch 'develop' into bendvc/W-18647820_fix-einstein-add-to-cart
bendvc Jun 17, 2025
492fe5c
Update packages/template-retail-react-app/CHANGELOG.md
larnelleankunda Jun 17, 2025
72279c2
Update packages/template-retail-react-app/CHANGELOG.md
larnelleankunda Jun 17, 2025
1860c67
Update change log
bendvc Jun 17, 2025
89ef272
Merge branch 'develop' into W-17599234-A11Y-email-modal-8
yunakim714 Jun 17, 2025
f61bfc9
Merge pull request #2558 from SalesforceCommerceCloud/bendvc/W-186478…
bendvc Jun 17, 2025
59f5209
Merge branch 'develop' into W-17599234-A11Y-email-modal-8
larnelleankunda Jun 17, 2025
fc14355
Fixing bad merge
larnelleankunda Jun 18, 2025
65e519b
Update sample query implementation in query.ts
shethj Jun 18, 2025
a8b3a66
Merge branch 'develop' into feature/handle-missing-sdk-clients
shethj Jun 18, 2025
95672ae
Merge pull request #2539 from SalesforceCommerceCloud/feature/handle-…
shethj Jun 18, 2025
33edb86
Merge branch 'develop' into W-17599234-A11Y-email-modal-8
larnelleankunda Jun 18, 2025
49c949f
Merge pull request #2540 from SalesforceCommerceCloud/W-17599234-A11Y…
larnelleankunda Jun 18, 2025
d45078a
[Fix E2E Tests] Improve E2E Test and Tracking Consent Banner Handling…
adamraya Jun 18, 2025
9abec74
rules only
szirpesf Jun 19, 2025
5083670
Update translations (#2616)
alexvuong Jun 20, 2025
4956da9
Merge branch 'develop' into sz-W-18800010-rules-only
szirpesf Jun 23, 2025
206e6d2
Merge pull request #2597 from SalesforceCommerceCloud/sz-W-18800010-r…
szirpesf Jun 23, 2025
8113896
@W-18541294@ Private client proxy updates (#2608)
vcua-mobify Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .cursor/rules/testing/unit-tests-generic.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
description: USE WHEN writing unit tests for components in template packages
globs: ["packages/template-*/*/components/**/*.test.{js,jsx,ts,tsx}"]
alwaysApply: false
---
USE WHEN writing unit tests for components in template packages

# 🧪 Generic Component Test Rules

# CRITICAL: AI Attribution Requirements
* **IMPORTANT** All individual test methods generated or modified by Cursor MUST include an AI attribution comment directly above the test stating the following:
"DO NOT REMOVE THIS COMMENT! This test was generated by Cursor". The comment should go on the test method and not the test class. Failure to add an AI Attribution Comment will be considered a failure of test generation.
* The AI attribution comment MUST include a comment declaring the LLM model that was used in writing the test on its own line.

*Sample AI Attribution Comment*
```
/*
* DO NOT REMOVE THIS COMMENT! This test was generated by Cursor
* This test was generated with the following model: Claude 3.5 Sonnet
*/
test('renders component correctly', () => {
// test implementation
})
```

## Structure & Best Practices
- Use `describe` blocks to group tests, `test` for individual cases
- Use `beforeEach` for setup, clear mocks after each test
- **Arrange** → **Act** → **Assert** pattern
- One behavior per test, clear descriptive names

## Queries & Assertions
- Prefer `getByRole`, `getByLabelText`, `getByTestId`
- Use `expect().toBeInTheDocument()`, `.toHaveBeenCalledTimes()`, etc.
- For async: `await waitFor(() => { ... })`

## Mocking
- `jest.fn()` for handlers, `jest.mock()` for modules
- Clear mocks/storage after each test

```js
describe('MyComponent', () => {
beforeEach(() => jest.clearAllMocks())

test('renders and handles interaction', async () => {
const mockHandler = jest.fn()
render(<MyComponent onClick={mockHandler} />)

await userEvent.click(screen.getByRole('button'))
expect(mockHandler).toHaveBeenCalledTimes(1)
})
})
```

## Running Tests
After creating unit tests, **ALWAYS run the tests** to verify they pass and provide feedback on test results.

### Command Format:
```bash
cd packages/<package-name> && npm run test -- '<relative-path-to-test-file> --coverage=false'
```

### Examples:
```bash
# Run specific test file from packages directory
cd packages/template-retail-react-app && npm run test -- 'app/components/drawer-menu/drawer-menu.test.js --coverage=false'
```

### After Running Tests:
- Report if tests **pass** or **fail**
- If tests fail, provide the error messages and fix any issues
- Confirm test coverage is appropriate for the component's core functionality
- Suggest any additional tests if critical functionality is missing
35 changes: 35 additions & 0 deletions .cursor/rules/testing/unit-tests-template-retail-react-app.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
description: USE WHEN writing unit tests in template-retail-react-app components
globs: ["packages/template-retail-react-app/app/components/**/*.test.{js,jsx,ts,tsx}"]
alwaysApply: false
---
# 🛍️ Retail React App Test Rules

## Package-Specific Requirements
- **File naming**: `index.test.js` (colocated with component)
- **Always use `renderWithProviders`** (provides Commerce SDK context)
- **Get user events from return value**: `const {user} = renderWithProviders(...)`
- **Do NOT import `userEvent` directly**

## API Mocking
- Use `prependHandlersToServer` or `msw` for API mocking

## Mock Data Usage

- **Mandatory**: Always use existing mock data from `@salesforce/retail-react-app/app/mocks/` if it is available. This ensures consistency across tests and reduces redundancy. Creating new mock data should only be considered if the required data is not already present in the mocks directory.

```js
import {screen} from '@testing-library/react'
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
import MyComponent from '.'

describe('MyComponent', () => {
beforeEach(() => jest.clearAllMocks())

test('handles user interaction', async () => {
const {user} = renderWithProviders(<MyComponent />)
await user.click(screen.getByText('Click Me'))
expect(screen.getByText('Expected')).toBeInTheDocument()
})
})
```
148 changes: 115 additions & 33 deletions e2e/scripts/pageHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,45 @@ const {getCreditCardExpiry, runAccessibilityTest} = require('../scripts/utils.js
* @param {Boolean} dnt - Do Not Track value to answer the form. False to enable tracking, True to disable tracking.
*/
export const answerConsentTrackingForm = async (page, dnt = false) => {
if ((await page.locator('text=Tracking Consent').count()) > 0) {
var text = 'Accept'
if (dnt) text = 'Decline'
const answerButton = await page.locator('button:visible', {hasText: text})
await expect(answerButton).toBeVisible()
await answerButton.click()
await expect(answerButton).not.toBeVisible()
try {
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 })

// 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"]`))

if (buttons.length === 0) {
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
if (button.offsetParent !== null) {
button.click()
clickedCount++
}
})

return clickedCount
}, buttonText)

// after clicking an answering button, the tracking consent should not stay in the DOM
const consentElements = await page.locator('text=Tracking Consent').count()
expect(consentElements).toBe(0)
if (clickSuccess > 0) {
await page.waitForTimeout(2000)
await page.locator('text=Tracking Consent').isHidden({ timeout: 5000 }).catch(() => {})
}
} catch (error) {
// Silently continue - consent form handling should not break tests
}
}

Expand Down Expand Up @@ -212,8 +241,23 @@ export const registerShopper = async ({page, userCredentials, isMobile = false})

await page.waitForLoadState()

// Skip registration if user is already logged in
const initialUrl = page.url()
if (initialUrl.includes('/account')) {
return
}

const registrationFormHeading = page.getByText(/Let's get started!/i)
await registrationFormHeading.waitFor()
try {
await registrationFormHeading.waitFor({ timeout: 10000 })
} catch (error) {
// Check if user was redirected to account page during wait
const urlAfterWait = page.url()
if (urlAfterWait.includes('/account')) {
return
}
throw new Error(`Registration form not found. Current URL: ${urlAfterWait}`)
}

await page.locator('input#firstName').fill(userCredentials.firstName)
await page.locator('input#lastName').fill(userCredentials.lastName)
Expand All @@ -226,16 +270,11 @@ export const registerShopper = async ({page, userCredentials, isMobile = false})
'**/shopper/auth/v1/organizations/**/oauth2/token'
)
await page.getByRole('button', {name: /Create Account/i}).click()
await tokenResponsePromise
expect((await tokenResponsePromise).status()).toBe(200)

await expect(page.getByRole('heading', {name: /Account Details/i})).toBeVisible()
const tokenResponse = await tokenResponsePromise
expect(tokenResponse.status()).toBe(200)

if (!isMobile) {
await expect(page.getByRole('heading', {name: /My Account/i})).toBeVisible()
}
await page.waitForURL(/.*\/account.*/, { timeout: 10000 })

await expect(page.getByText(/Email/i)).toBeVisible()
await expect(page.getByText(userCredentials.email)).toBeVisible()
}

Expand Down Expand Up @@ -310,12 +349,18 @@ export const loginShopper = async ({page, userCredentials}) => {
'**/shopper/auth/v1/organizations/**/oauth2/token'
)
await page.getByRole('button', {name: /Sign In/i}).click()
await loginResponsePromise
expect((await loginResponsePromise).status()).toBe(303) // Login returns a 303 redirect to /callback with authCode and usid
await tokenResponsePromise
expect((await tokenResponsePromise).status()).toBe(200)

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 expect(page.getByText(userCredentials.email)).toBeVisible()
return true
} catch {
} catch (error) {
return false
}
}
Expand Down Expand Up @@ -484,8 +529,13 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials,
userCredentials: registeredUserCredentials
})
}
await answerConsentTrackingForm(page)
await page.waitForLoadState()
await expect(page.getByRole('heading', {name: /Account Details/i})).toBeVisible()

// Verify we're on account page and user is logged in
const currentUrl = page.url()
expect(currentUrl).toMatch(/\/account/)
await expect(page.getByText(registeredUserCredentials.email)).toBeVisible()

// Shop for items as registered user
await addProductToCart({page})
Expand Down Expand Up @@ -535,14 +585,20 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials,
name: /Continue to Payment/i
})

if (continueToPayment.isEnabled()) {
let hasShippingStep = false
try {
await expect(continueToPayment).toBeVisible({timeout: 2000})
await continueToPayment.click()
hasShippingStep = true
} catch {
// Shipping step was skipped, proceed directly to payment
}

// Confirm the shipping options form toggles to show edit button on clicking "Checkout as guest"
const step2Card = page.locator("div[data-testid='sf-toggle-card-step-2']")

await expect(step2Card.getByRole('button', {name: /Edit/i})).toBeVisible()
// Verify step-2 edit button only if shipping step was present
if (hasShippingStep) {
const step2Card = page.locator("div[data-testid='sf-toggle-card-step-2']")
await expect(step2Card.getByRole('button', {name: /Edit/i})).toBeVisible()
}

await expect(page.getByRole('heading', {name: /Payment/i})).toBeVisible()

Expand Down Expand Up @@ -585,23 +641,49 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials,
await validateOrderHistory({page, a11y})
}

/**
* 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
* @param {Object} options.registeredUserCredentials - User credentials for authentication
* @param {Object} options.a11y - Accessibility testing configuration (optional)
*/
export const wishlistFlow = async ({page, registeredUserCredentials, a11y = {}}) => {
const isLoggedIn = await loginShopper({
page,
userCredentials: registeredUserCredentials
})

if (!isLoggedIn) {
await registerShopper({
page,
userCredentials: registeredUserCredentials
})
try {
await registerShopper({
page,
userCredentials: registeredUserCredentials
})
} catch (error) {
// If registration fails attempt to log in
const secondLoginAttempt = await loginShopper({
page,
userCredentials: registeredUserCredentials
})
if (!secondLoginAttempt) {
throw new Error('Authentication failed: Both login and registration unsuccessful')
}
}
}

// The consent form does not stick after registration
await answerConsentTrackingForm(page)
await page.waitForLoadState()

const currentUrl = page.url()
if (!currentUrl.includes('/account')) {
await page.goto(config.RETAIL_APP_HOME + '/account')
await page.waitForLoadState()
}

// Navigate to PDP
await navigateToPDPDesktop({page})

Expand Down
3 changes: 2 additions & 1 deletion e2e/tests/desktop/dnt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
const {test, expect} = require('@playwright/test')
const config = require('../../config.js')
const {generateUserCredentials} = require('../../scripts/utils.js')
const {registerShopper} = require('../../scripts/pageHelpers.js')
const {registerShopper, answerConsentTrackingForm} = require('../../scripts/pageHelpers.js')

const REGISTERED_USER_CREDENTIALS = generateUserCredentials()

Expand Down Expand Up @@ -55,6 +55,7 @@ test('Shopper can use the consent tracking form', async ({page}) => {

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

// Logging out clears the preference
Expand Down
12 changes: 8 additions & 4 deletions e2e/tests/homepage.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ test.describe('Retail app home page loads', () => {
})

test('get started link', async ({page}) => {
await page.getByRole('link', {name: 'Get started'}).click()
const getStartedLink = page.getByRole('link', {name: 'Get started'})
await expect(getStartedLink).toBeVisible()

const getStartedPage = await page.waitForEvent('popup')
await getStartedPage.waitForLoadState()
const popupPromise = page.waitForEvent('popup', { timeout: 30000 })
await getStartedLink.click()

await expect(getStartedPage).toHaveURL(/.*getting-started/)
const getStartedPage = await popupPromise
await expect(getStartedPage).toHaveURL(/.*getting-started/, { timeout: 15000 })

await expect(getStartedPage.getByRole('heading').first()).toBeVisible({ timeout: 10000 })
})
})
3 changes: 2 additions & 1 deletion e2e/tests/mobile/dnt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
const {test, expect} = require('@playwright/test')
const config = require('../../config.js')
const {generateUserCredentials} = require('../../scripts/utils.js')
const {registerShopper} = require('../../scripts/pageHelpers.js')
const {registerShopper, answerConsentTrackingForm} = require('../../scripts/pageHelpers.js')

const REGISTERED_USER_CREDENTIALS = generateUserCredentials()

Expand Down Expand Up @@ -69,6 +69,7 @@ test('Shopper can use the consent tracking form', async ({page}) => {

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

// Logging out clears the preference
Expand Down
Loading
Loading