Skip to content

@W-20224360 Passkey login E2E tests#3636

Merged
hajinsuha1 merged 7 commits intofeature/webauthn-loginfrom
W-20224360-e2e-tests-passkey-login
Feb 11, 2026
Merged

@W-20224360 Passkey login E2E tests#3636
hajinsuha1 merged 7 commits intofeature/webauthn-loginfrom
W-20224360-e2e-tests-passkey-login

Conversation

@hajinsuha1
Copy link
Collaborator

@hajinsuha1 hajinsuha1 commented Feb 4, 2026

Description

Adds Passkey Login E2E Tests

Note:
Need to make a PR to update the extra-features-e2e-branch (example PR)

  • enable passkey
  • setup client ID with relying party

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Breaking changes include:

  • Removing a public function or component or prop
  • Adding a required argument to a function
  • Changing the data type of a function parameter or return value
  • Adding a new peer dependency to package.json

Changes

  • Add passkey login E2E tests for desktop and mobile

How to Test-Drive This PR

  1. Verify E2E Tests pass
    # Set the playwright tests to run against a `template-retail-react-app` with the new passkey login feature enabled
    export EXTRA_FEATURES_E2E_RETAIL_APP_HOME=https://wasatch-mrt-passwordless-test.mrt-storefront-staging.com
    
    npx playwright test --project=extra-features-desktop --project=extra-features-mobile --ui
    
  2. Verify the Passkey Login tests pass in mobile and desktop
    Screenshot 2026-02-06 at 12 26 32 PM

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

- Consolidate passwordless login tests into a single describe block for better organization.
@hajinsuha1 hajinsuha1 added the skip changelog Skip the "Changelog Check" GitHub Actions step even if the Changelog.md files are not updated label Feb 4, 2026
@cc-prodsec
Copy link
Collaborator

cc-prodsec commented Feb 4, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

- Introduced a new function `validatePasskeyLogin` in `pageHelpers.js` to simulate passkey authentication using a virtual authenticator.
- Updated the registration and login functions to accept a `siteUrl` parameter for flexibility.
- Added E2E tests for passkey login in both desktop and mobile test suites to ensure proper functionality.
@hajinsuha1 hajinsuha1 changed the base branch from develop to feature/webauthn-login February 6, 2026 17:23
@hajinsuha1 hajinsuha1 marked this pull request as ready for review February 6, 2026 17:27
@hajinsuha1 hajinsuha1 requested a review from a team as a code owner February 6, 2026 17:27
(route) => {
interceptedRequest = route.request()
route.continue()
describe('Passwordless login', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the original tests are unchanged and have just been moved to describe blocks for organization

@hajinsuha1
Copy link
Collaborator Author

hajinsuha1 commented Feb 6, 2026

note: tests are failing in CI because we are using commerce-sdk-isomorphic preview version but that is unrelated to this change as these are E2E tests

@jeremy-jung1
Copy link
Collaborator

When I run the tests locally, I'm seeing that the test for both desktop and mobile are hanging at "Wait for event "response"". I'm also finding that sometimes the test shows it's passing, even when we never got the response from finishWebauthnAuth.

Do you see any indication that the response has not been received on your end when you run the tests?

# Test info

tests/desktop/extra-features.spec.js:212 › Passkey Login › Verify passkey login

# Error details

Error: page.waitForResponse: Test timeout of 60000ms exceeded.
=========================== logs ===========================
waiting for response "**/mobify/slas/private/shopper/auth/v1/organizati…"

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - generic [ref=e4]:
    - link [ref=e5] [cursor=pointer]:
      - /url: "#chakra-skip-nav"
      - text: Skip to Content
    - banner [ref=e7]:
      - generic [ref=e9]:
        - button [ref=e10] [cursor=pointer]:
          - img [ref=e11]
        - navigation "Main navigation" [ref=e14]:
          - generic [ref=e16]:
            - generic [ref=e19]:
              - link "New Arrivals" [ref=e20] [cursor=pointer]:
                - /url: /global/en-GB/category/newarrivals
              - link "chevron-down" [ref=e21] [cursor=pointer]:
                - /url: /global/en-GB
                - img "chevron-down" [ref=e23]
            - generic [ref=e26]:
              - link "Womens" [ref=e27] [cursor=pointer]:
                - /url: /global/en-GB/category/womens
              - link "chevron-down" [ref=e28] [cursor=pointer]:
                - /url: /global/en-GB
                - img "chevron-down" [ref=e30]
            - generic [ref=e33]:
              - link "Mens" [ref=e34] [cursor=pointer]:
                - /url: /global/en-GB/category/mens
              - link "chevron-down" [ref=e35] [cursor=pointer]:
                - /url: /global/en-GB
                - img "chevron-down" [ref=e37]
            - link "Gift Certificates" [ref=e39] [cursor=pointer]:
              - /url: /global/en-GB/category/gift-certificates
            - link "Top Sellers" [ref=e41] [cursor=pointer]:
              - /url: /global/en-GB/category/top-seller
        - generic [ref=e47]:
          - generic:
            - img
          - searchbox [ref=e48]
        - button [ref=e49] [cursor=pointer]:
          - img [ref=e50]
        - button [ref=e51] [cursor=pointer]:
          - img [ref=e52]
        - button [ref=e53] [cursor=pointer]:
          - img [ref=e54]
        - button [ref=e55] [cursor=pointer]:
          - img [ref=e56]
    - main [ref=e58]:
      - generic [ref=e59]:
        - heading [level=1] [ref=e60]: Sign In
        - generic [ref=e61]:
          - generic [ref=e62]:
            - img [ref=e63]
            - paragraph [ref=e64]: Welcome Back
          - generic [ref=e66]:
            - generic [ref=e67]:
              - group [ref=e69]:
                - generic [ref=e70]: Email
                - textbox [ref=e72]:
                  - /placeholder: you@email.com
              - button [ref=e73] [cursor=pointer]: Continue
              - separator [ref=e74]
              - paragraph [ref=e75]: Or Login With
              - button [ref=e77] [cursor=pointer]: Password
            - generic [ref=e78]:
              - paragraph [ref=e79]: Don't have an account?
              - button [ref=e80] [cursor=pointer]: Create account
    - contentinfo [ref=e81]:
      - generic [ref=e82]:
        - generic [ref=e84]:
          - generic [ref=e85]:
            - heading [level=2] [ref=e86]: Customer Support
            - list [ref=e87]:
              - listitem [ref=e88]:
                - link [ref=e89] [cursor=pointer]:
                  - /url: /
                  - text: Contact Us
              - listitem [ref=e90]:
                - link [ref=e91] [cursor=pointer]:
                  - /url: /
                  - text: Shipping
          - generic [ref=e92]:
            - heading [level=2] [ref=e93]: Account
            - list [ref=e94]:
              - listitem [ref=e95]:
                - link [ref=e96] [cursor=pointer]:
                  - /url: /
                  - text: Order Status
              - listitem [ref=e97]:
                - link [ref=e98] [cursor=pointer]:
                  - /url: /
                  - text: Sign in or create account
          - generic [ref=e99]:
            - heading [level=2] [ref=e100]: Our Company
            - list [ref=e101]:
              - listitem [ref=e102]:
                - link [ref=e103] [cursor=pointer]:
                  - /url: /global/en-GB/store-locator
                  - text: Store Locator
              - listitem [ref=e104]:
                - link [ref=e105] [cursor=pointer]:
                  - /url: /
                  - text: About Us
          - generic [ref=e107]:
            - heading [level=2] [ref=e108]: Be the first to know
            - paragraph [ref=e109]: Sign up to stay in the loop about the hottest deals
            - generic [ref=e111]:
              - button [ref=e113] [cursor=pointer]: Sign Up
              - textbox [ref=e114]:
                - /placeholder: you@email.com
            - generic [ref=e115]:
              - button [ref=e116] [cursor=pointer]:
                - img [ref=e117]
              - button [ref=e118] [cursor=pointer]:
                - img [ref=e119]
              - button [ref=e120] [cursor=pointer]:
                - img [ref=e121]
              - button [ref=e122] [cursor=pointer]:
                - img [ref=e123]
              - button [ref=e124] [cursor=pointer]:
                - img [ref=e125]
        - group [ref=e127]:
          - generic [ref=e128]:
            - combobox [ref=e129]
            - generic:
              - img
        - separator [ref=e130]
        - generic [ref=e131]:
          - paragraph [ref=e132]: © 2026 Salesforce or its affiliates. All rights reserved. This is a demo store only. Orders made WILL NOT be processed.
          - list [ref=e135]:
            - listitem [ref=e136]:
              - link [ref=e137] [cursor=pointer]:
                - /url: /
                - text: Terms & Conditions
            - listitem [ref=e138]:
              - link [ref=e139] [cursor=pointer]:
                - /url: /
                - text: Privacy Policy
            - listitem [ref=e140]:
              - link [ref=e141] [cursor=pointer]:
                - /url: /
                - text: Site Map
  - generic:
    - region
    - region
    - region
    - region
    - region
    - region
  - dialog [ref=e142]:
    - generic [ref=e143]:
      - button "Close consent tracking form" [ref=e144] [cursor=pointer]:
        - img [ref=e145]
      - generic [ref=e147]:
        - heading "Tracking Consent" [level=3] [ref=e148]
        - generic [ref=e150]:
          - paragraph [ref=e151]: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
          - generic [ref=e152]:
            - button "Decline tracking" [ref=e153] [cursor=pointer]: Decline
            - button "Accept tracking" [ref=e154] [cursor=pointer]: Accept

Test source

  758 |     await expect(searchStoreButton).toBeVisible()
  759 | 
  760 |     const storeSearchResponsePromise = page.waitForResponse(
  761 |         (resp) =>
  762 |             resp.url().includes('/shopper-stores/v1/organizations/') &&
  763 |             resp.url().includes('/store-search')
  764 |     )
  765 |     await searchStoreButton.click()
  766 |     const storeSearchResponse = await storeSearchResponsePromise
  767 | 
  768 |     expect(storeSearchResponse.status()).toBe(200)
  769 | 
  770 |     // Select the first available store (if any stores are available)
  771 |     await expect(page.getByText(/Burlington Retail Store/i)).toBeVisible()
  772 | 
  773 |     // Find and click the first available store label
  774 |     const storeRadioLabels = page.locator(
  775 |         'label.chakra-radio:has(input[aria-describedby^="store-info-"])'
  776 |     )
  777 |     const storeCount = await storeRadioLabels.count()
  778 | 
  779 |     if (storeCount > 0) {
  780 |         // Select the first store
  781 |         await storeRadioLabels.first().click()
  782 | 
  783 |         // Close the store locator modal
  784 |         await page.locator('button[aria-label="Close"]').click()
  785 |         await page.waitForLoadState()
  786 |         await expect(page.getByText('Find a Store')).not.toBeVisible()
  787 |     } else {
  788 |         // If no stores are available, verify the appropriate message is shown
  789 |         await expect(page.getByText('Sorry, there are no locations in this area.')).toBeVisible()
  790 | 
  791 |         // Close the modal
  792 |         await page.getByRole('button', {name: 'Close'}).click()
  793 |     }
  794 | }
  795 | 
  796 | /**
  797 |  * Validates that a passkey login request is made to the /webAuthn/authenticate/finish endpoint.
  798 |  * We can't register an actual passkey in the E2E environment because registration requires a token verification.
  799 |  * Instead,we add a mock credential to the virtual authenticator to bypass the registration flow and verify the
  800 |  * request to the /webAuthn/authenticate/finish endpoint.
  801 |  *
  802 |  * @param {Object} options.page - Playwright page object representing a browser tab/window
  803 |  */
  804 | export const validatePasskeyLogin = async ({page}) => {
  805 |     // Start a CDP session to interact with WebAuthn
  806 |     const client = await page.context().newCDPSession(page)
  807 |     await client.send('WebAuthn.enable')
  808 |     // Create a virtual authenticator to simulate a hardware authenticator for testing
  809 |     const {authenticatorId} = await client.send('WebAuthn.addVirtualAuthenticator', {
  810 |         options: {
  811 |             protocol: 'ctap2',
  812 |             transport: 'internal',
  813 |             hasResidentKey: true,
  814 |             hasUserVerification: true,
  815 |             isUserVerified: true,
  816 |             // Enabling automaticPresenceSimulation automatically completes the device's passkey prompt without user interaction
  817 |             automaticPresenceSimulation: true
  818 |         }
  819 |     })
  820 | 
  821 |     // Preload mock credential into the virtual authenticator
  822 |     const rpId = new URL(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME).hostname
  823 |     // Generate a valid EC key pair for WebAuthn (ES256/P-256)
  824 |     const {privateKey} = crypto.generateKeyPairSync('ec', {namedCurve: 'P-256'})
  825 |     const privateKeyBase64 = privateKey.export({format: 'der', type: 'pkcs8'}).toString('base64')
  826 | 
  827 |     console.log('privateKeyBase64', privateKeyBase64)
  828 |     const credentialIdBuffer = Buffer.from('mock-credential-id-' + Date.now())
  829 |     const credentialIdBase64 = credentialIdBuffer.toString('base64') // For mock credential
  830 |     const credentialId = credentialIdBuffer.toString('base64url') // For verifying the request
  831 |     await client.send('WebAuthn.addCredential', {
  832 |         authenticatorId,
  833 |         credential: {
  834 |             credentialId: credentialIdBase64,
  835 |             isResidentCredential: true,
  836 |             rpId,
  837 |             privateKey: privateKeyBase64,
  838 |             userHandle: Buffer.from('test-user-handle').toString('base64'),
  839 |             signCount: 0,
  840 |             transports: ['internal']
  841 |         }
  842 |     })
  843 | 
  844 |     let interceptedRequest = null
  845 | 
  846 |     // Intercept the WebAuthn authenticate/finish endpoint to verify the request
  847 |     await page.route(
  848 |         '**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/webauthn/authenticate/finish',
  849 |         (route) => {
  850 |             interceptedRequest = route.request()
  851 |             route.continue()
  852 |         }
  853 |     )
  854 | 
  855 |     await page.goto(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME + '/login')
  856 | 
  857 |     // Wait for the WebAuthn authenticate/finish request
> 858 |     await page.waitForResponse(
      |                ^ Error: page.waitForResponse: Test timeout of 60000ms exceeded.
  859 |         '**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/webauthn/authenticate/finish'
  860 |     )
  861 | 
  862 |     // Verify the /webAuthn/authenticate/finish request
  863 |     expect(interceptedRequest).toBeTruthy()
  864 |     expect(interceptedRequest.method()).toBe('POST')
  865 |     const postData = interceptedRequest.postData()
  866 |     expect(postData).toBeTruthy()
  867 |     const requestBody = JSON.parse(postData)
  868 |     expect(requestBody).toBeTruthy()
  869 | 
  870 |     // Verify the request body structure matches expected format
  871 |     expect(requestBody.client_id).toBeTruthy()
  872 |     expect(requestBody.channel_id).toBe(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME_SITE)
  873 |     expect(requestBody.credential.id).toBe(credentialId)
  874 |     expect(requestBody.credential.clientExtensionResults).toBeTruthy()
  875 |     expect(requestBody.credential.response).toBeTruthy()
  876 | }
  877 | 

- Added retry logic to passkey login tests in both desktop and mobile test suites to handle authentication cooldowns.
@hajinsuha1
Copy link
Collaborator Author

hajinsuha1 commented Feb 10, 2026

@jeremy-jung1 i've added a timeout of 60s before the test runs again on a retry. The retries are already configured in playwright.config.js. Once SLAS releases with the 1min timeout instead this should work as expected.

To run the tests with retries we have to run in headless mode

export EXTRA_FEATURES_E2E_RETAIL_APP_HOME=https://wasatch-mrt-passwordless-test.mrt-storefront-staging.com
npx playwright test --project=extra-features-desktop --project=extra-features-mobile
Screenshot 2026-02-10 at 2 35 45 PM

- Removed unnecessary retry logic in passkey login tests for both desktop and mobile test suites.
- Updated comments for clarity regarding the authentication cooldown period.
…s to 60 seconds, while overriding it to 70 seconds for specific passkey login tests to accommodate cooldown periods.
@hajinsuha1 hajinsuha1 merged commit e293a52 into feature/webauthn-login Feb 11, 2026
4 of 42 checks passed
@hajinsuha1 hajinsuha1 deleted the W-20224360-e2e-tests-passkey-login branch February 11, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip changelog Skip the "Changelog Check" GitHub Actions step even if the Changelog.md files are not updated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants