diff --git a/frontend/src/lib/stores/session.svelte.test.ts b/frontend/src/lib/stores/session.svelte.test.ts index b026ddf1..20325726 100644 --- a/frontend/src/lib/stores/session.svelte.test.ts +++ b/frontend/src/lib/stores/session.svelte.test.ts @@ -8,7 +8,8 @@ const { fetchMeMock } = vi.hoisted(() => ({ fetchMeMock: vi.fn() })); vi.mock('$lib/api/endpoints/auth', () => ({ fetchMe: () => fetchMeMock(), - tryRefresh: vi.fn(async () => false) + tryRefresh: vi.fn(async () => false), + bindDpopIfPossible: vi.fn(async () => false) })); import { session } from './session.svelte'; diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index fcb911f7..e84ae0dd 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -424,8 +424,26 @@ // avoids stealing focus from something else during the loading // splash; the input-ref guard covers the render-order case where // the effect fires before the DOM has the target. + // + // `activeElement` guard: if the user (or Playwright's `.fill()`, or + // browser autofill) already has focus in a form field, don't yank + // it away. Concrete bug this prevents: boot probes are slow → user + // types their email into the (initially unfocused) input → probes + // finish → `booting` flips false → this effect fires and refocuses + // the input, which resets the caret and can concatenate subsequent + // keystrokes onto the wrong field if the user was mid-tab. Mode- + // swap re-runs still refocus correctly because the old form's + // inputs unmount first, resetting `activeElement` to ``. $effect(() => { if (booting) return; + const active = document.activeElement; + if ( + active && + active !== document.body && + (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') + ) { + return; + } const target = mode === 'login' ? loginIdentifierInput diff --git a/frontend/src/routes/login/page.test.ts b/frontend/src/routes/login/page.test.ts index 58c1be35..6d62bb66 100644 --- a/frontend/src/routes/login/page.test.ts +++ b/frontend/src/routes/login/page.test.ts @@ -21,6 +21,7 @@ vi.mock('$app/navigation', () => ({ goto })); vi.mock('$app/state', () => ({ page: pageState })); vi.mock('$lib/stores/session.svelte', () => ({ session })); vi.mock('$lib/api/endpoints/auth', () => ({ + bindDpopIfPossible: vi.fn().mockResolvedValue(false), exchangeOidcCode: vi.fn(), fetchMe: vi.fn(), getOidcProviders: vi.fn(),