Skip to content

Commit 659ddd6

Browse files
Erick Teowarangclaude
andcommitted
test(e2e): cover OAuth callback flash + hook-isolation in AuthProvider
Extend the auth Playwright suite with a regression test that walks the full login flow and asserts: - the auth guard is not in the DOM after the callback resolves (no sign-in flash during the OAuth callback exchange), and - no React hook-order warnings are logged during the auth state transition (the E2E app's <AuthGuard> uses `useAuth`, the exact shape that previously inlined a hook into AuthGuard's hook list). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f838e0b commit 659ddd6

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

e2e/tests/auth.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,68 @@ test.describe("AuthProvider", () => {
8383
});
8484
});
8585

86+
test("OAuth callback does not flash the auth guard or trip React rules of hooks", async ({
87+
page,
88+
}) => {
89+
// Regression for two interacting bugs:
90+
// 1. AuthGuard used to render guardComponent during a pending OAuth
91+
// callback when the auth client surfaced a stale isReady &&
92+
// !isAuthenticated state — flashing the sign-in screen the user
93+
// just returned from.
94+
// 2. AuthGuard invoked the slots as plain function calls, which
95+
// inlined slot hooks into AuthGuard's own hook scope. Because
96+
// the slots render conditionally, any slot that called a hook
97+
// (this app's <AuthGuard> calls useAuth) would change AuthGuard's
98+
// hook order across renders → "React has detected a change in
99+
// the order of Hooks called by AuthGuard".
100+
const consoleErrors: string[] = [];
101+
page.on("console", (msg) => {
102+
if (msg.type() === "error") consoleErrors.push(msg.text());
103+
});
104+
page.on("pageerror", (err) => consoleErrors.push(err.message));
105+
106+
await page.goto("/");
107+
await page.getByTestId("login-button").click();
108+
109+
const email = process.env.E2E_USER_EMAIL;
110+
const password = process.env.E2E_USER_PASSWORD;
111+
112+
if (!email || !password) {
113+
test.skip(true, "E2E_USER_EMAIL and E2E_USER_PASSWORD must be set");
114+
return;
115+
}
116+
117+
await page.waitForURL(/idp\.erp\.dev\/.*\/signin/);
118+
await page.getByLabel(/email/i).fill(email);
119+
await page.locator("#password").fill(password);
120+
await page.getByRole("button", { name: /sign in|log in|submit/i }).click();
121+
122+
await page.waitForURL("http://localhost:3100/**");
123+
await expect(page.getByTestId("authenticated-content")).toBeVisible({
124+
timeout: 10000,
125+
});
126+
127+
// Callback blackout: after we land back on the app, the sign-in
128+
// screen must not be in the DOM at all — the AuthProvider should
129+
// have rendered nothing during the callback exchange and then
130+
// transitioned directly to authenticated content.
131+
await expect(page.getByTestId("auth-guard")).toHaveCount(0);
132+
133+
// Hooks isolation: the slot (AuthGuard, which uses useAuth) was
134+
// rendered at least once during the unauthenticated state. If the
135+
// slot were invoked as a plain function call instead of as its own
136+
// fiber, the transition out of !isAuthenticated would log a hook-
137+
// order warning here.
138+
const reactErrors = consoleErrors.filter(
139+
(e) =>
140+
e.includes("order of Hooks") ||
141+
e.includes("Invalid hook call") ||
142+
e.includes("Rendered fewer hooks") ||
143+
e.includes("Rendered more hooks"),
144+
);
145+
expect(reactErrors, `Unexpected React hook errors:\n${reactErrors.join("\n")}`).toEqual([]);
146+
});
147+
86148
test("maintains session on page reload after login", async ({ page }) => {
87149
await page.goto("/");
88150
await page.getByTestId("login-button").click();

0 commit comments

Comments
 (0)