diff --git a/apps/web/test/setup.ts b/apps/web/test/setup.ts index 2564691c80..9881d55857 100644 --- a/apps/web/test/setup.ts +++ b/apps/web/test/setup.ts @@ -11,6 +11,11 @@ if (typeof globalThis.ResizeObserver === "undefined") { } as unknown as typeof ResizeObserver; } +// jsdom doesn't implement elementFromPoint; input-otp uses it internally. +if (typeof document.elementFromPoint !== "function") { + document.elementFromPoint = () => null; +} + // jsdom 29 / Node.js 22+ may not provide a proper Web Storage API. // Create a proper localStorage mock if methods are missing. if ( diff --git a/packages/views/auth/login-page.test.tsx b/packages/views/auth/login-page.test.tsx index cc1d374e42..b351c09315 100644 --- a/packages/views/auth/login-page.test.tsx +++ b/packages/views/auth/login-page.test.tsx @@ -198,6 +198,23 @@ describe("LoginPage", () => { expect(screen.getByText(/test@example.com/)).toBeInTheDocument(); }); + it("autofocuses the OTP input when the code step opens", async () => { + mockSendCode.mockResolvedValueOnce(undefined); + renderWithI18n(); + + const user = userEvent.setup(); + await user.type(screen.getByLabelText(/email/i), "test@example.com"); + await user.click(screen.getByRole("button", { name: /continue/i })); + + await waitFor(() => { + expect(screen.getByText(/check your email/i)).toBeInTheDocument(); + }); + + // The OTP field should be focused on mount so the user can type the code + // without clicking it first — important when repeatedly switching accounts. + expect(getOTPInput()).toHaveFocus(); + }); + it("shows error when sendCode fails", async () => { mockSendCode.mockRejectedValueOnce(new Error("Rate limited")); renderWithI18n(); diff --git a/packages/views/auth/login-page.tsx b/packages/views/auth/login-page.tsx index 23f1bf486a..5a25e22ed0 100644 --- a/packages/views/auth/login-page.tsx +++ b/packages/views/auth/login-page.tsx @@ -349,6 +349,7 @@ export function LoginPage({ {