Skip to content

Commit b5e7dc8

Browse files
sf-emmyzhangshethjlarnelleankundakevinxhyunakim714
authored
merge develop into standard product feature branch (#2626)
* Handle SDK Client not initialized * Added tests for useResolvedClient * Handle missing SDK Client ShopperContext * ShopperCustomers - Handle SDK Client no init * ShopperExperience - Handle missing SDK Client * ShopperGiftCertificates - Handle missing SDK Client * ShopperLogin: Handle missing SDK client * ShopperOrders - Handle Missing SDK Client * ShopperProducts: Handle missing SDK Client * ShopperPromotions: Handle missing SDK client * ShopperSearch - Handle missing SDK Client * ShopperSEO: Handle missing SDK Client * ShopperStores: Handle missing SDK Client * Fix linting errors * Update useResolvedClient tests * Update changelog * implemented the voiceover feature for the email confirmation modal on PWA kit. * Update changelog * Extract CLIENT_KEY literal to constnt * updated the chanege log file to point to current changes and saved changes to email confirmation modal code * linted the files and built translations * changed role and aria label tags to solve unsuccessful check issues * Implemented changes to create more successful checks after facing a testing Library ElementError: Found multiple elements with the text: /check your email/i * Linted my files * Remove additional hook for client validation * Fix linting * Add comments * rename apiClients variable to add clarity Co-authored-by: Kevin He <kevin.he@salesforce.com> Signed-off-by: Jainam Sheth <99490559+shethj@users.noreply.github.com> * update variable name for apiClients * linted more files to fix unsuccessful check error * linted more files to fix unsuccessful check error * Update packages/template-retail-react-app/CHANGELOG.md Co-authored-by: Yuna Kim <84923642+yunakim714@users.noreply.github.com> Signed-off-by: Larnelle Ankunda <lankunda@salesforce.com> * taking off the visible focus outline around the modal border * Initial Commit Follow `plugin_einstein_api` implementation for event data creation. * Update CHANGELOG.md * Update CHANGELOG.md * Add some additional tests * PR Feedback * adding an a11y tag to changes made docuemented in the change log reading document * Fix bar reference to master.variantId * removed redundant aria label that repeating email confirmation title * mitigating the need for additional translations * feat: cursor rules for unit tests * Update packages/template-retail-react-app/CHANGELOG.md Co-authored-by: Yuna Kim <84923642+yunakim714@users.noreply.github.com> Signed-off-by: Larnelle Ankunda <lankunda@salesforce.com> * Update packages/template-retail-react-app/CHANGELOG.md Co-authored-by: Yuna Kim <84923642+yunakim714@users.noreply.github.com> Signed-off-by: Larnelle Ankunda <lankunda@salesforce.com> * Update change log * Fixing bad merge * Update sample query implementation in query.ts * [Fix E2E Tests] Improve E2E Test and Tracking Consent Banner Handling (@W-18764173@) (#2575) * Improve e2e tests and Tracking Consent banner handling * Clean up * More clean up * Remove spacing changes * PR Feedback * Remove comments * rules only * Update translations (#2616) * update translations * @W-18541294@ Private client proxy updates (#2608) * Ensure only requests to /shopper/auth/ are allowed by the SLAS private client proxy * Remove console.logs * Stop swallowing errors from SLAS * Update CHANGELOG.md * Fix login and logout * Remove console logs * Lint * Add and fix tests * Cleanup regex --------- Signed-off-by: Jainam Sheth <99490559+shethj@users.noreply.github.com> Signed-off-by: Larnelle Ankunda <lankunda@salesforce.com> Signed-off-by: Ben Chypak <bchypak@mobify.com> Co-authored-by: Jainam Sheth <j.sheth@salesforce.com> Co-authored-by: Jainam Sheth <99490559+shethj@users.noreply.github.com> Co-authored-by: Larnelle Ankunda <lankunda@salesforce.com> Co-authored-by: Kevin He <kevin.he@salesforce.com> Co-authored-by: Yuna Kim <84923642+yunakim714@users.noreply.github.com> Co-authored-by: Ben Chypak <bchypak@salesforce.com> Co-authored-by: Ben Chypak <bchypak@mobify.com> Co-authored-by: Shailesh Zirpe <szirpe+sf@salesforce.com> Co-authored-by: Shailesh Zirpe <42553862+szirpesf@users.noreply.github.com> Co-authored-by: Adam Raya <adamraya@users.noreply.github.com> Co-authored-by: Alex Vuong <alex.vuong@salesforce.com> Co-authored-by: vcua-mobify <47404250+vcua-mobify@users.noreply.github.com>
1 parent 22bbb48 commit b5e7dc8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+6340
-1456
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
description: USE WHEN writing unit tests for components in template packages
3+
globs: ["packages/template-*/*/components/**/*.test.{js,jsx,ts,tsx}"]
4+
alwaysApply: false
5+
---
6+
USE WHEN writing unit tests for components in template packages
7+
8+
# 🧪 Generic Component Test Rules
9+
10+
# CRITICAL: AI Attribution Requirements
11+
* **IMPORTANT** All individual test methods generated or modified by Cursor MUST include an AI attribution comment directly above the test stating the following:
12+
"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.
13+
* The AI attribution comment MUST include a comment declaring the LLM model that was used in writing the test on its own line.
14+
15+
*Sample AI Attribution Comment*
16+
```
17+
/*
18+
* DO NOT REMOVE THIS COMMENT! This test was generated by Cursor
19+
* This test was generated with the following model: Claude 3.5 Sonnet
20+
*/
21+
test('renders component correctly', () => {
22+
// test implementation
23+
})
24+
```
25+
26+
## Structure & Best Practices
27+
- Use `describe` blocks to group tests, `test` for individual cases
28+
- Use `beforeEach` for setup, clear mocks after each test
29+
- **Arrange** → **Act** → **Assert** pattern
30+
- One behavior per test, clear descriptive names
31+
32+
## Queries & Assertions
33+
- Prefer `getByRole`, `getByLabelText`, `getByTestId`
34+
- Use `expect().toBeInTheDocument()`, `.toHaveBeenCalledTimes()`, etc.
35+
- For async: `await waitFor(() => { ... })`
36+
37+
## Mocking
38+
- `jest.fn()` for handlers, `jest.mock()` for modules
39+
- Clear mocks/storage after each test
40+
41+
```js
42+
describe('MyComponent', () => {
43+
beforeEach(() => jest.clearAllMocks())
44+
45+
test('renders and handles interaction', async () => {
46+
const mockHandler = jest.fn()
47+
render(<MyComponent onClick={mockHandler} />)
48+
49+
await userEvent.click(screen.getByRole('button'))
50+
expect(mockHandler).toHaveBeenCalledTimes(1)
51+
})
52+
})
53+
```
54+
55+
## Running Tests
56+
After creating unit tests, **ALWAYS run the tests** to verify they pass and provide feedback on test results.
57+
58+
### Command Format:
59+
```bash
60+
cd packages/<package-name> && npm run test -- '<relative-path-to-test-file> --coverage=false'
61+
```
62+
63+
### Examples:
64+
```bash
65+
# Run specific test file from packages directory
66+
cd packages/template-retail-react-app && npm run test -- 'app/components/drawer-menu/drawer-menu.test.js --coverage=false'
67+
```
68+
69+
### After Running Tests:
70+
- Report if tests **pass** or **fail**
71+
- If tests fail, provide the error messages and fix any issues
72+
- Confirm test coverage is appropriate for the component's core functionality
73+
- Suggest any additional tests if critical functionality is missing
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
description: USE WHEN writing unit tests in template-retail-react-app components
3+
globs: ["packages/template-retail-react-app/app/components/**/*.test.{js,jsx,ts,tsx}"]
4+
alwaysApply: false
5+
---
6+
# 🛍️ Retail React App Test Rules
7+
8+
## Package-Specific Requirements
9+
- **File naming**: `index.test.js` (colocated with component)
10+
- **Always use `renderWithProviders`** (provides Commerce SDK context)
11+
- **Get user events from return value**: `const {user} = renderWithProviders(...)`
12+
- **Do NOT import `userEvent` directly**
13+
14+
## API Mocking
15+
- Use `prependHandlersToServer` or `msw` for API mocking
16+
17+
## Mock Data Usage
18+
19+
- **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.
20+
21+
```js
22+
import {screen} from '@testing-library/react'
23+
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
24+
import MyComponent from '.'
25+
26+
describe('MyComponent', () => {
27+
beforeEach(() => jest.clearAllMocks())
28+
29+
test('handles user interaction', async () => {
30+
const {user} = renderWithProviders(<MyComponent />)
31+
await user.click(screen.getByText('Click Me'))
32+
expect(screen.getByText('Expected')).toBeInTheDocument()
33+
})
34+
})
35+
```

e2e/scripts/pageHelpers.js

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,45 @@ const {getCreditCardExpiry, runAccessibilityTest} = require('../scripts/utils.js
1818
* @param {Boolean} dnt - Do Not Track value to answer the form. False to enable tracking, True to disable tracking.
1919
*/
2020
export const answerConsentTrackingForm = async (page, dnt = false) => {
21-
if ((await page.locator('text=Tracking Consent').count()) > 0) {
22-
var text = 'Accept'
23-
if (dnt) text = 'Decline'
24-
const answerButton = await page.locator('button:visible', {hasText: text})
25-
await expect(answerButton).toBeVisible()
26-
await answerButton.click()
27-
await expect(answerButton).not.toBeVisible()
21+
try {
22+
const consentFormVisible = await page.locator('text=Tracking Consent').isVisible().catch(() => false)
23+
if (!consentFormVisible) {
24+
return
25+
}
26+
27+
const buttonText = dnt ? 'Decline' : 'Accept'
28+
await page.getByRole('button', { name: new RegExp(buttonText, 'i') }).first().waitFor({ timeout: 3000 })
29+
30+
// Find and click consent buttons (handles both mobile and desktop versions existing in the DOM)
31+
const clickSuccess = await page.evaluate((targetText) => {
32+
// Try aria-label first, then fallback to text content
33+
let buttons = Array.from(document.querySelectorAll(`button[aria-label="${targetText} tracking"]`))
34+
35+
if (buttons.length === 0) {
36+
buttons = Array.from(document.querySelectorAll('button')).filter(btn =>
37+
btn.textContent && btn.textContent.trim().toLowerCase() === targetText.toLowerCase()
38+
)
39+
}
40+
41+
let clickedCount = 0
42+
buttons.forEach((button) => {
43+
// Only click visible buttons
44+
if (button.offsetParent !== null) {
45+
button.click()
46+
clickedCount++
47+
}
48+
})
49+
50+
return clickedCount
51+
}, buttonText)
52+
2853
// after clicking an answering button, the tracking consent should not stay in the DOM
29-
const consentElements = await page.locator('text=Tracking Consent').count()
30-
expect(consentElements).toBe(0)
54+
if (clickSuccess > 0) {
55+
await page.waitForTimeout(2000)
56+
await page.locator('text=Tracking Consent').isHidden({ timeout: 5000 }).catch(() => {})
57+
}
58+
} catch (error) {
59+
// Silently continue - consent form handling should not break tests
3160
}
3261
}
3362

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

213242
await page.waitForLoadState()
214243

244+
// Skip registration if user is already logged in
245+
const initialUrl = page.url()
246+
if (initialUrl.includes('/account')) {
247+
return
248+
}
249+
215250
const registrationFormHeading = page.getByText(/Let's get started!/i)
216-
await registrationFormHeading.waitFor()
251+
try {
252+
await registrationFormHeading.waitFor({ timeout: 10000 })
253+
} catch (error) {
254+
// Check if user was redirected to account page during wait
255+
const urlAfterWait = page.url()
256+
if (urlAfterWait.includes('/account')) {
257+
return
258+
}
259+
throw new Error(`Registration form not found. Current URL: ${urlAfterWait}`)
260+
}
217261

218262
await page.locator('input#firstName').fill(userCredentials.firstName)
219263
await page.locator('input#lastName').fill(userCredentials.lastName)
@@ -226,16 +270,11 @@ export const registerShopper = async ({page, userCredentials, isMobile = false})
226270
'**/shopper/auth/v1/organizations/**/oauth2/token'
227271
)
228272
await page.getByRole('button', {name: /Create Account/i}).click()
229-
await tokenResponsePromise
230-
expect((await tokenResponsePromise).status()).toBe(200)
231-
232-
await expect(page.getByRole('heading', {name: /Account Details/i})).toBeVisible()
273+
const tokenResponse = await tokenResponsePromise
274+
expect(tokenResponse.status()).toBe(200)
233275

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

238-
await expect(page.getByText(/Email/i)).toBeVisible()
239278
await expect(page.getByText(userCredentials.email)).toBeVisible()
240279
}
241280

@@ -310,12 +349,18 @@ export const loginShopper = async ({page, userCredentials}) => {
310349
'**/shopper/auth/v1/organizations/**/oauth2/token'
311350
)
312351
await page.getByRole('button', {name: /Sign In/i}).click()
313-
await loginResponsePromise
314-
expect((await loginResponsePromise).status()).toBe(303) // Login returns a 303 redirect to /callback with authCode and usid
315-
await tokenResponsePromise
316-
expect((await tokenResponsePromise).status()).toBe(200)
352+
353+
const loginResponse = await loginResponsePromise
354+
expect(loginResponse.status()).toBe(303) // Login returns a 303 redirect to /callback with authCode and usid
355+
356+
const tokenResponse = await tokenResponsePromise
357+
expect(tokenResponse.status()).toBe(200)
358+
359+
await page.waitForURL(/.*\/account.*/, { timeout: 10000 })
360+
361+
await expect(page.getByText(userCredentials.email)).toBeVisible()
317362
return true
318-
} catch {
363+
} catch (error) {
319364
return false
320365
}
321366
}
@@ -484,8 +529,13 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials,
484529
userCredentials: registeredUserCredentials
485530
})
486531
}
532+
await answerConsentTrackingForm(page)
487533
await page.waitForLoadState()
488-
await expect(page.getByRole('heading', {name: /Account Details/i})).toBeVisible()
534+
535+
// Verify we're on account page and user is logged in
536+
const currentUrl = page.url()
537+
expect(currentUrl).toMatch(/\/account/)
538+
await expect(page.getByText(registeredUserCredentials.email)).toBeVisible()
489539

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

538-
if (continueToPayment.isEnabled()) {
588+
let hasShippingStep = false
589+
try {
590+
await expect(continueToPayment).toBeVisible({timeout: 2000})
539591
await continueToPayment.click()
592+
hasShippingStep = true
593+
} catch {
594+
// Shipping step was skipped, proceed directly to payment
540595
}
541596

542-
// Confirm the shipping options form toggles to show edit button on clicking "Checkout as guest"
543-
const step2Card = page.locator("div[data-testid='sf-toggle-card-step-2']")
544-
545-
await expect(step2Card.getByRole('button', {name: /Edit/i})).toBeVisible()
597+
// Verify step-2 edit button only if shipping step was present
598+
if (hasShippingStep) {
599+
const step2Card = page.locator("div[data-testid='sf-toggle-card-step-2']")
600+
await expect(step2Card.getByRole('button', {name: /Edit/i})).toBeVisible()
601+
}
546602

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

@@ -585,23 +641,49 @@ export const registeredUserHappyPath = async ({page, registeredUserCredentials,
585641
await validateOrderHistory({page, a11y})
586642
}
587643

644+
/**
645+
* Executes the wishlist flow for a registered user.
646+
*
647+
* Includes robust authentication handling with fallback mechanisms.
648+
*
649+
* @param {Object} options.page - Playwright page object representing a browser tab/window
650+
* @param {Object} options.registeredUserCredentials - User credentials for authentication
651+
* @param {Object} options.a11y - Accessibility testing configuration (optional)
652+
*/
588653
export const wishlistFlow = async ({page, registeredUserCredentials, a11y = {}}) => {
589654
const isLoggedIn = await loginShopper({
590655
page,
591656
userCredentials: registeredUserCredentials
592657
})
593658

594659
if (!isLoggedIn) {
595-
await registerShopper({
596-
page,
597-
userCredentials: registeredUserCredentials
598-
})
660+
try {
661+
await registerShopper({
662+
page,
663+
userCredentials: registeredUserCredentials
664+
})
665+
} catch (error) {
666+
// If registration fails attempt to log in
667+
const secondLoginAttempt = await loginShopper({
668+
page,
669+
userCredentials: registeredUserCredentials
670+
})
671+
if (!secondLoginAttempt) {
672+
throw new Error('Authentication failed: Both login and registration unsuccessful')
673+
}
674+
}
599675
}
600676

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

681+
const currentUrl = page.url()
682+
if (!currentUrl.includes('/account')) {
683+
await page.goto(config.RETAIL_APP_HOME + '/account')
684+
await page.waitForLoadState()
685+
}
686+
605687
// Navigate to PDP
606688
await navigateToPDPDesktop({page})
607689

e2e/tests/desktop/dnt.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
const {test, expect} = require('@playwright/test')
99
const config = require('../../config.js')
1010
const {generateUserCredentials} = require('../../scripts/utils.js')
11-
const {registerShopper} = require('../../scripts/pageHelpers.js')
11+
const {registerShopper, answerConsentTrackingForm} = require('../../scripts/pageHelpers.js')
1212

1313
const REGISTERED_USER_CREDENTIALS = generateUserCredentials()
1414

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

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

6061
// Logging out clears the preference

e2e/tests/homepage.spec.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ test.describe('Retail app home page loads', () => {
2020
})
2121

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

25-
const getStartedPage = await page.waitForEvent('popup')
26-
await getStartedPage.waitForLoadState()
26+
const popupPromise = page.waitForEvent('popup', { timeout: 30000 })
27+
await getStartedLink.click()
2728

28-
await expect(getStartedPage).toHaveURL(/.*getting-started/)
29+
const getStartedPage = await popupPromise
30+
await expect(getStartedPage).toHaveURL(/.*getting-started/, { timeout: 15000 })
31+
32+
await expect(getStartedPage.getByRole('heading').first()).toBeVisible({ timeout: 10000 })
2933
})
3034
})

e2e/tests/mobile/dnt.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
const {test, expect} = require('@playwright/test')
99
const config = require('../../config.js')
1010
const {generateUserCredentials} = require('../../scripts/utils.js')
11-
const {registerShopper} = require('../../scripts/pageHelpers.js')
11+
const {registerShopper, answerConsentTrackingForm} = require('../../scripts/pageHelpers.js')
1212

1313
const REGISTERED_USER_CREDENTIALS = generateUserCredentials()
1414

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

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

7475
// Logging out clears the preference

0 commit comments

Comments
 (0)