e2e/auth-callback-locale.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
test.use({ locale: "zh-CN" });
for (const scenario of [
{
name: "different account language",
language: "ja",
htmlLang: "ja-JP",
continueLabel: "ウェブで続ける",
},
{
name: "same account language",
language: "zh-Hans",
htmlLang: "zh-CN",
continueLabel: "在 web 端继续",
},
]) {
test(`Google callback keeps its single-use code while synchronizing ${scenario.name}`, async ({
page,
context,
baseURL,
}) => {
if (!baseURL) {
throw new Error("The callback regression requires a configured baseURL");
}
const origin = new URL(baseURL).origin;
const code = "locale-regression-code";
const user = {
id: "66666666-6666-4666-8666-666666666666",
name: "Callback locale test",
email: "callback-locale@example.com",
avatar_url: null,
language: scenario.language,
onboarded_at: null,
onboarding_questionnaire: {},
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
};
let authenticated = false;
let workspaceRequests = 0;
let callbackDocuments = 0;
const exchanges: unknown[] = [];
const unexpectedApiRequests: string[] = [];
const googleRequests: string[] = [];
const pageErrors: string[] = [];
let completeInitialAuthCheck!: () => void;
const initialAuthCheck = new Promise<void>((resolve) => {
completeInitialAuthCheck = resolve;
});
let releaseWorkspaces!: () => void;
const workspaceGate = new Promise<void>((resolve) => {
releaseWorkspaces = resolve;
});
await context.addCookies([
{ name: "multica-locale", value: "zh-Hans", url: origin },
]);
// Isolate application WebSockets without intercepting Next.js HMR.
await context.routeWebSocket(
(url) => url.pathname === "/ws",
(socket) => socket.close(),
);
page.on("pageerror", (error) => pageErrors.push(error.message));
page.on("request", (request) => {
if (
request.isNavigationRequest() &&
request.frame() === page.mainFrame() &&
new URL(request.url()).pathname === "/auth/callback"
) {
callbackDocuments += 1;
}
});
// Keep the real Next.js page, providers, auth store, and API client. Only
// the HTTP boundary is controlled; no Google account or backend rows are used.
await context.route("**/*", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.origin !== origin) {
if (
/(^|\.)(google\.com|googleapis\.com|googleusercontent\.com)$/.test(url.hostname)
) {
googleRequests.push(request.url());
}
return route.abort();
}
const json = (status: number, body: unknown) => route.fulfill({ status, json: body });
if (url.pathname === "/auth/google" && request.method() === "POST") {
exchanges.push(request.postDataJSON());
// A replay must fail, otherwise an accidental callback reload can look successful.
if (exchanges.length > 1) {
return json(400, {
code: "oauth_code_invalid",
error: "The test code has already been used",
});
}
await initialAuthCheck;
authenticated = true;
return json(200, { token: "synthetic-callback-token", user });
}
if (url.pathname === "/api/config") {
return json(200, {
allow_signup: true,
google_client_id: "callback-test-only",
feature_flags: {},
});
}
if (url.pathname === "/api/me") {
if (authenticated) return json(200, user);
await json(401, { error: "Not authenticated" });
completeInitialAuthCheck();
return;
}
if (url.pathname === "/api/workspaces") {
workspaceRequests += 1;
await workspaceGate;
return json(200, []);
}
if (url.pathname === "/api/invitations") return json(200, []);
if (url.pathname === "/api/client-usage") return route.fulfill({ status: 204 });
if (url.pathname.startsWith("/api/") || url.pathname === "/auth/google") {
unexpectedApiRequests.push(`${request.method()} ${url.pathname}`);
return json(500, { error: "Unexpected API request in callback locale test" });
}
return route.continue();
});
try {
await page.goto(`/auth/callback?code=${code}`, { waitUntil: "domcontentloaded" });
await expect.poll(() => workspaceRequests).toBeGreaterThan(0);
// Let the login render and its passive effects finish while navigation is
// blocked by the workspace response, instead of racing a fixed network delay.
await page.evaluate(async () => {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
});
await expect(page).toHaveURL(`${origin}/auth/callback?code=${code}`);
expect(exchanges).toEqual([{ code, redirect_uri: `${origin}/auth/callback` }]);
expect(callbackDocuments).toBe(1);
expect(
(await context.cookies(origin)).find((cookie) => cookie.name === "multica-locale")?.value,
).toBe("zh-Hans");
releaseWorkspaces();
await expect(page).toHaveURL(`${origin}/onboarding`, { timeout: 15000 });
await expect(page.locator("html")).toHaveAttribute("lang", scenario.htmlLang);
await expect(
page.getByRole("button", { name: scenario.continueLabel, exact: true }),
).toBeVisible();
expect(
(await context.cookies(origin)).find((cookie) => cookie.name === "multica-locale")?.value,
).toBe(scenario.language);
expect(await page.evaluate(() => localStorage.getItem("multica_token"))).toBeNull();
expect(exchanges).toHaveLength(1);
expect(callbackDocuments).toBe(1);
expect(unexpectedApiRequests).toEqual([]);
expect(googleRequests).toEqual([]);
expect(pageErrors).toEqual([]);
} finally {
releaseWorkspaces();
await context.unrouteAll({ behavior: "wait" });
}
});
}