From c02ced432dc5feabc0ba7078390a5664a08910e0 Mon Sep 17 00:00:00 2001 From: Zaid Marji Date: Sun, 24 May 2026 10:23:45 +0300 Subject: [PATCH] playwright: fix TOTP setup flow against bundled web vault The `activateTOTP` helper had three issues against the current bundled web vault (v2026.4.1): 1. The master-password reprompt's Continue button click was racing form validation: the form uses Angular's `updateOn: 'blur'` so clicking immediately after `fill()` saw an "invalid" form and the click was silently no-op. Submitting via `mpInput.press('Enter')` triggers the form's `ngSubmit` directly, which validates and submits in one go. 2. `getByLabel('Key').innerText()` was ambiguous: the same page has a `` providers entry whose accessible name contains "Key" as a substring. Strict-mode violation on Firefox. Anchor with `{ exact: true }` to pick only the `` element holding the base32 secret. 3. After clicking "Turn on", the original code was effectively a no-op (`await page.getByRole('heading', { name: 'Turned on' })` creates a Locator without awaiting visibility), then clicked the "Close" button which is `bit-aria-disable=true` until the dialog finishes its transition. Chromium happens to tolerate the early click; Firefox doesn't. Wait for `networkidle` after Turn on so the activation request completes; drop the Close click because the dialog auto-closes on success on this web vault. `disableTOTP` got the same reprompt-via-Enter fix for parity. Verified empirically: with these changes, `login.spec.ts`'s `Authenticator 2fa` test passes on Firefox against bundled web-vault v2026.4.1. --- playwright/tests/setups/2fa.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/playwright/tests/setups/2fa.ts b/playwright/tests/setups/2fa.ts index d3e68830..60b1ae54 100644 --- a/playwright/tests/setups/2fa.ts +++ b/playwright/tests/setups/2fa.ts @@ -11,16 +11,27 @@ export async function activateTOTP(test: Test, page: Page, user: { name: string, await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); - await page.getByLabel('Master password').fill(user.password); - await page.getByRole('button', { name: 'Continue' }).click(); + const mpInput = page.getByLabel('Master password'); + await mpInput.fill(user.password); + // Submit via Enter — Angular form validation can race a click on + // the Continue button immediately after fill on the current + // bundled web vault. + await mpInput.press('Enter'); - const secret = await page.getByLabel('Key').innerText(); + // `getByLabel('Key')` alone is ambiguous: the providers list also + // has a Yubico SVG with aria-label "Yubico OTP security key" that + // matches "Key" via substring. Anchor with exact match. + const secret = (await page.getByLabel('Key', { exact: true }).innerText()).replace(/\s+/g, ''); let totp = new OTPAuth.TOTP({ secret, period: 30 }); await page.getByLabel(/Verification code/).fill(totp.generate()); await page.getByRole('button', { name: 'Turn on' }).click(); - await page.getByRole('heading', { name: 'Turned on', exact: true }); - await page.getByLabel('Close').click(); + // Wait for the activation request to complete. The current + // bundled web vault uses an asynchronous Turn-on flow; we don't + // try to assert the exact success-heading text (it varies across + // vault versions) — instead we wait for network to settle, then + // the dialog closes itself. + await page.waitForLoadState('networkidle'); return totp; }) @@ -33,9 +44,9 @@ export async function disableTOTP(test: Test, page: Page, user: { password: stri await page.getByRole('link', { name: 'Security' }).click(); await page.getByRole('link', { name: 'Two-step login' }).click(); await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); - await page.getByLabel('Master password').click(); - await page.getByLabel('Master password').fill(user.password); - await page.getByRole('button', { name: 'Continue' }).click(); + const mpInput = page.getByLabel('Master password'); + await mpInput.fill(user.password); + await mpInput.press('Enter'); await page.getByRole('button', { name: 'Turn off' }).click(); await page.getByRole('button', { name: 'Yes' }).click(); await utils.checkNotification(page, 'Two-step login provider turned off');