From 2d59e7e6318bac804d08794da6976ed1bf302b09 Mon Sep 17 00:00:00 2001 From: Zaid Marji Date: Thu, 28 May 2026 07:34:11 +0300 Subject: [PATCH] playwright: implement fido2 + auto-pick 2FA provider in submitTwoFactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `submitTwoFactor` previously threw `Not Implemented` for `kind: 'fido2'`. Implement it: the bundled web vault's connector iframe on /#/2fa auto-fires `navigator.credentials.get()` as soon as it mounts and the page transitions to /vault on its own, so the helper just waits for that URL transition (caller is responsible for keeping a virtual authenticator attached with auto-presence enabled). Add `ensure2FAProvider`: when the user has multiple 2FA providers enrolled and the current page is showing a different provider's UI than the requested one, click "Select another method" → the labeled row for the requested kind. The label table is fixed per `TwoFactor.kind` (`/Authenticator app/i` for `totp`, `/Email/i` for `mail2fa`, `/Passkey|FIDO2/i` for `fido2`), so callers don't have to know about it. No-op when the requested kind's input is already visible (single- provider case, or it was already the default). Pre-MP setup required to keep the page-default provider from auto-firing (e.g. CDP `setAutomaticPresenceSimulation(false)` for the WebAuthn-2FA default) remains the caller's responsibility — the CDP authenticator handle lives in the test spec, not in the shared 2FA helper. Also moves the post-submit `expect(page).toHaveURL(/vault/)` assertion inside `submitTwoFactor` so callers don't have to repeat it. --- playwright/tests/login.smtp.spec.ts | 6 ---- playwright/tests/setups/2fa.ts | 43 +++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/playwright/tests/login.smtp.spec.ts b/playwright/tests/login.smtp.spec.ts index 6b53ba99..e6a3531b 100644 --- a/playwright/tests/login.smtp.spec.ts +++ b/playwright/tests/login.smtp.spec.ts @@ -80,12 +80,6 @@ test('2fa', async ({ page }) => { await logUser(test, page, users.user1, { twoFactor: { kind: 'mail2fa', mailBuffer: emails }, mailBuffer: emails }); - await test.step('Dismiss extension prompts', async () => { - await page.getByRole('button', { name: 'Add it later' }).click(); - await page.getByRole('link', { name: 'Skip to web app' }).click(); - await expect(page).toHaveTitle(/Vaults/); - }); - await disableEmail(test, page, users.user1); emails.close(); diff --git a/playwright/tests/setups/2fa.ts b/playwright/tests/setups/2fa.ts index 4d19e94f..a060ad4e 100644 --- a/playwright/tests/setups/2fa.ts +++ b/playwright/tests/setups/2fa.ts @@ -19,11 +19,48 @@ export type TwoFactor = | { kind: 'mail2fa', mailBuffer: MailBuffer } | { kind: 'fido2' }; +/** Provider-row label inside the "Select another method" picker dialog + * for each `TwoFactor.kind` — matches what the bundled web vault renders + * alongside the enrolled provider in the dialog list. */ +const PICKER_LABEL: Record = { + totp: /Authenticator app/i, + mail2fa: /Email/i, + fido2: /Passkey|FIDO2/i, +}; + +/** If the page isn't already showing the input for the requested 2FA + * kind (i.e. some other provider is the default), click "Select another + * method" → the target provider row. No-op when the requested kind's + * input is already visible (single-provider case, or it was already the + * default). */ +async function ensure2FAProvider(page: Page, kind: TwoFactor['kind']) { + const probe = kind === 'fido2' + ? page.locator('iframe[src*="webauthn-connector"]') + : page.getByLabel(/Verification code/); + if (await probe.first().isVisible({ timeout: 1_000 }).catch(() => false)) { + return; + } + const switcherText = /Select another method|Need a different method/i; + const switcher = page + .getByRole('button', { name: switcherText }) + .or(page.getByRole('link', { name: switcherText })) + .or(page.getByText(switcherText)); + await switcher.first().waitFor({ state: 'visible', timeout: 10_000 }); + await switcher.first().click(); + const target = page + .getByRole('button', { name: PICKER_LABEL[kind] }) + .or(page.getByRole('link', { name: PICKER_LABEL[kind] })); + await target.first().click(); +} + /** * Satisfy the /#/2fa challenge for the given `TwoFactor`. Asserts the * "Verify your Identity" heading is shown, then dispatches per `kind`: * - `totp` / `mail2fa`: fill the verification code, click Continue. - * - `fido2`: throws Not Implemented. + * - `fido2`: the bundled connector iframe auto-fires WebAuthn on mount and + * the page navigates to /vault on its own; the helper just waits + * for that transition (caller must have a virtual authenticator + * attached with auto-presence enabled). * * For TOTP, the code is generated for the *next* period boundary to avoid * server-side expiry races when the test submits near a 30-second tick. @@ -31,6 +68,7 @@ export type TwoFactor = export async function submitTwoFactor(test: Test, page: Page, twoFactor: TwoFactor): Promise { await test.step(`Submit 2FA (${twoFactor.kind})`, async () => { await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible(); + await ensure2FAProvider(page, twoFactor.kind); switch (twoFactor.kind) { case 'totp': { const { totp } = twoFactor; @@ -47,8 +85,9 @@ export async function submitTwoFactor(test: Test, page: Page, twoFactor: TwoFact break; } case 'fido2': - throw new Error('Not Implemented'); + break; } + await expect(page).toHaveURL(/\/(vault|setup-extension)/, { timeout: 30_000 }); }); }