diff --git a/tests/e2e/campaignWebAuthn.e2e.spec.ts b/tests/e2e/campaignWebAuthn.e2e.spec.ts index 1324ffc1..db6d0b59 100644 --- a/tests/e2e/campaignWebAuthn.e2e.spec.ts +++ b/tests/e2e/campaignWebAuthn.e2e.spec.ts @@ -2,7 +2,7 @@ import type { BrowserContext, Page } from '@playwright/test' import { CAMPAIGN_SESSION_TTL_LONG } from '../../src/lib/campaignSessionTtl.js' import { CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY } from '../../src/utilities/campaignBiometricsPrompt.js' -import { expect, test } from './fixtures/campaignE2EFixtures.js' +import { expect, expectCampaignBiometricsReady, test } from './fixtures/campaignE2EFixtures.js' /** * Biometric login (B40) end to end, against a **virtual authenticator** driven @@ -68,8 +68,10 @@ test('enrolls a passkey, signs in with it, then revokes it', async ({ await addVirtualAuthenticator(context, page) // 1. Enrollment is authenticated: the passkey card only exists behind a - // password login. + // password login. The platform answer is gated BEFORE the profile island + // runs its one-shot probe (miss #53). await campaign.login(page, email, password) + await expectCampaignBiometricsReady(page) await page.goto('/campanha/perfil') await expect(page.getByText('Nenhum aparelho cadastrado ainda.')).toBeVisible() @@ -86,6 +88,7 @@ test('enrolls a passkey, signs in with it, then revokes it', async ({ // the "remember me" opt-in, so there is no checkbox in this path. await context.clearCookies() await page.goto('/campanha/login') + await expectCampaignBiometricsReady(page) const biometricButton = page.getByRole('button', { name: 'Entrar com digital ou Face ID' }) await expect(biometricButton).toBeVisible() diff --git a/tests/e2e/fixtures/campaignE2EFixtures.ts b/tests/e2e/fixtures/campaignE2EFixtures.ts index b5befc84..643da103 100644 --- a/tests/e2e/fixtures/campaignE2EFixtures.ts +++ b/tests/e2e/fixtures/campaignE2EFixtures.ts @@ -474,6 +474,25 @@ export const checkRadixWhenHydrated = async (page: Page, label: string) => { }).toPass({ timeout: 15_000 }) } +/** + * Miss #53: the app's biometrics probe is ONE-SHOT per island mount + * (`useCampaignBiometricsAvailable`) — if + * `isUserVerifyingPlatformAuthenticatorAvailable()` resolves before the CDP + * virtual authenticator is ready, the enrollment form / biometric button + * never mount and no amount of waiting flips them. Gate the ceremony on the + * platform answer BEFORE navigating into the surface whose island probes it, + * so the race is decided before the app even asks. Requires a loaded page + * (secure context) — call after `campaign.login` / `page.goto`. + */ +export const expectCampaignBiometricsReady = async (page: Page): Promise => { + await expect(async () => { + const available = await page.evaluate( + () => window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable?.() ?? false, + ) + expect(available).toBe(true) + }).toPass({ timeout: 15_000 }) +} + type CampaignE2EFixture = { baseURL: string fixtures: CampaignE2EOwnership