fix(e2e): WebAuthn on prod build, saved-filters navigation, Vercel scripts

CI e2e serves a production build on localhost; Next inlines NODE_ENV so
WebAuthn RP resolution now treats localhost requests as test mode. Saved
filters avoids a racing bare goto. Analytics/SpeedInsights render only on Vercel.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-30 17:27:22 -03:00
parent 865c9e0a32
commit 30170509f7
4 changed files with 36 additions and 7 deletions

View file

@ -77,8 +77,12 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
>
<body className="antialiased w-screen scrollbar-hide overflow-hidden">
<ThemeProvider attribute="class">
<SpeedInsights />
<Analytics />
{process.env.VERCEL === '1' ? (
<>
<SpeedInsights />
<Analytics />
</>
) : null}
<RefreshRouteOnSave />
{children}
</ThemeProvider>

View file

@ -17,6 +17,12 @@ export type CampaignWebAuthnRelyingParty = {
const RP_NAME = 'Campanha Jorge Solla'
/** Matches `LOCAL_AUTHORITY_PATTERN` in `campaignInviteOrigin` — kept local on purpose. */
const LOCAL_REQUEST_HOST = /^(localhost|127\.0\.0\.1|\[::1\])(?::[0-9]{1,5})?$/i
const isLocalRequestHost = (host: string | undefined): boolean =>
Boolean(host && LOCAL_REQUEST_HOST.test(host))
/**
* The authority the browser is actually talking to, which is what has to agree
* with the relying party. Compared as `host` (name + port) and not as a full
@ -61,9 +67,17 @@ const requestHostOf = ({
export const getCampaignWebAuthnRelyingParty = (
input: CampaignInviteOriginInput = {},
): CampaignWebAuthnRelyingParty | null => {
const requestHost = requestHostOf(input)
// `getCampaignInviteBaseURL` reads `process.env.NODE_ENV`, which Next inlines
// at build time as `"production"`. CI e2e serves that build on
// http://localhost:3000 (`pnpm start`), which would otherwise demand HTTPS
// DNS and hide biometrics. The request host is authoritative here.
const environment =
input.environment ?? (isLocalRequestHost(requestHost) ? ('test' as const) : undefined)
let baseURL: string
try {
baseURL = getCampaignInviteBaseURL(input)
baseURL = getCampaignInviteBaseURL(environment ? { ...input, environment } : input)
} catch {
return null
}
@ -74,7 +88,6 @@ export const getCampaignWebAuthnRelyingParty = (
// Absent host: a non-request context (a script, a test) that cannot be
// checked. The ceremonies always pass the headers.
const requestHost = requestHostOf(input)
if (requestHost && requestHost !== url.host.toLowerCase()) return null
return { rpID, rpName: RP_NAME, origin: url.origin }

View file

@ -55,11 +55,14 @@ test.describe('Filtros salvos de Municípios', () => {
await expect(shortcut).toHaveAttribute('aria-current', 'page')
// A page is a position inside the recorte, not part of it.
await page.goto(`${SAVED_RECORTE}&page=2`)
await page.goto(`${SAVED_RECORTE}&page=2`, NAVIGATION)
await expect(shortcut).toHaveAttribute('aria-current', 'page')
// Another recorte is not this one, and the shortcut brings it back.
await page.goto('/campanha/municipios')
// Another recorte is not this one, and the shortcut brings it back. Use the
// sidebar link — a bare `goto` here races an in-flight RSC navigation and
// flakes as net::ERR_ABORTED under prod load (measured in CI 2026-07-30).
await page.getByRole('link', { name: 'Municípios', exact: true }).click()
await expect(page).toHaveURL('/campanha/municipios', NAVIGATION)
await expect(shortcut).not.toHaveAttribute('aria-current', 'page')
await shortcut.click()
await expect(page).toHaveURL(SAVED_RECORTE, NAVIGATION)

View file

@ -46,6 +46,15 @@ describe('getCampaignWebAuthnRelyingParty', () => {
).toMatchObject({ rpID: 'localhost', origin: 'http://localhost:3100' })
})
it('follows localhost when a production build is served there (CI e2e)', () => {
expect(
getCampaignWebAuthnRelyingParty({
configuredURL: 'https://pt.jorgesolla.com.br',
forwardedHost: 'localhost:3000',
}),
).toMatchObject({ rpID: 'localhost', origin: 'http://localhost:3000' })
})
it('refuses production without a configured site URL', () => {
expect(
getCampaignWebAuthnRelyingParty({ environment: 'production', configuredURL: undefined }),