fix: harden frontend state and tooling
This commit is contained in:
@@ -12,18 +12,25 @@ const responses: Record<string, unknown> = {
|
||||
notifyBefore: true,
|
||||
notifyAfter: true,
|
||||
},
|
||||
accounts: [{
|
||||
id: 1,
|
||||
displayName: "默认账号",
|
||||
email: "test@example.com",
|
||||
planType: "plus",
|
||||
expectedKind: "personal",
|
||||
actualKind: "personal",
|
||||
validationStatus: "matched",
|
||||
possibleDuplicate: false,
|
||||
connected: true,
|
||||
}],
|
||||
"settings/telegram": { token: "", configured: true, enabled: true, menuEnabled: true },
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
displayName: "默认账号",
|
||||
email: "test@example.com",
|
||||
planType: "plus",
|
||||
expectedKind: "personal",
|
||||
actualKind: "personal",
|
||||
validationStatus: "matched",
|
||||
possibleDuplicate: false,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
"settings/telegram": {
|
||||
token: "",
|
||||
configured: true,
|
||||
enabled: true,
|
||||
menuEnabled: true,
|
||||
},
|
||||
"settings/smtp": {
|
||||
host: "smtp.example.com",
|
||||
port: 587,
|
||||
@@ -45,7 +52,9 @@ async function openSettings(page: Page) {
|
||||
await route.fulfill({ json: responses[key] ?? {} });
|
||||
});
|
||||
await page.goto("/settings");
|
||||
await expect(page.locator("#settings-panel-smtp h2", { hasText: "SMTP 邮件" })).toBeAttached();
|
||||
await expect(
|
||||
page.locator("#settings-panel-smtp h2", { hasText: "SMTP 邮件" }),
|
||||
).toBeAttached();
|
||||
await expect(page.locator(".settings-loading")).toHaveCount(0);
|
||||
}
|
||||
|
||||
@@ -58,7 +67,9 @@ test("shows the build version in the authenticated brand", async ({ page }) => {
|
||||
|
||||
test("shows the build version before setup", async ({ page }) => {
|
||||
await page.route("**/api/v1/system/status", (route) =>
|
||||
route.fulfill({ json: { initialized: false, appServer: false, version: "test" } }),
|
||||
route.fulfill({
|
||||
json: { initialized: false, appServer: false, version: "test" },
|
||||
}),
|
||||
);
|
||||
await page.goto("/");
|
||||
const badge = page.locator(
|
||||
@@ -72,7 +83,9 @@ test("shows the build version before setup", async ({ page }) => {
|
||||
|
||||
test("shows the build version on login", async ({ page }) => {
|
||||
await page.route("**/api/v1/system/status", (route) =>
|
||||
route.fulfill({ json: { initialized: true, appServer: false, version: "test" } }),
|
||||
route.fulfill({
|
||||
json: { initialized: true, appServer: false, version: "test" },
|
||||
}),
|
||||
);
|
||||
await page.route("**/api/v1/auth/me", (route) =>
|
||||
route.fulfill({ status: 401, json: { error: "unauthorized" } }),
|
||||
@@ -89,11 +102,18 @@ async function layout(page: Page) {
|
||||
const rect = document.querySelector(selector)!.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||
};
|
||||
return { content: box(".settings-content"), tabs: box(".tabs"), heading: box("header h1"), scrollY: window.scrollY };
|
||||
return {
|
||||
content: box(".settings-content"),
|
||||
tabs: box(".tabs"),
|
||||
heading: box("header h1"),
|
||||
scrollY: window.scrollY,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
test("switching every settings panel keeps the layout stable", async ({ page }) => {
|
||||
test("switching every settings panel keeps the layout stable", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSettings(page);
|
||||
const initial = await layout(page);
|
||||
for (const name of ["Codex", "Telegram", "SMTP 邮件", "通用"]) {
|
||||
@@ -102,7 +122,9 @@ test("switching every settings panel keeps the layout stable", async ({ page })
|
||||
}
|
||||
});
|
||||
|
||||
test("switching panels preserves form state and isolates hidden controls", async ({ page }) => {
|
||||
test("switching panels preserves form state and isolates hidden controls", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSettings(page);
|
||||
const timezone = page.getByLabel("时区");
|
||||
await timezone.fill("Asia/Shanghai");
|
||||
@@ -116,14 +138,19 @@ test("switching panels preserves form state and isolates hidden controls", async
|
||||
await expect(timezone).toHaveValue("Asia/Shanghai");
|
||||
});
|
||||
|
||||
test("arrow, Home, and End keys move focus and activate tabs", async ({ page }) => {
|
||||
test("arrow, Home, and End keys move focus and activate tabs", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSettings(page);
|
||||
const general = page.getByRole("tab", { name: "通用" });
|
||||
await general.focus();
|
||||
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await expect(page.getByRole("tab", { name: "Codex" })).toBeFocused();
|
||||
await expect(page.getByRole("tab", { name: "Codex" })).toHaveAttribute("aria-selected", "true");
|
||||
await expect(page.getByRole("tab", { name: "Codex" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
|
||||
await page.keyboard.press("End");
|
||||
await expect(page.getByRole("tab", { name: "SMTP 邮件" })).toBeFocused();
|
||||
@@ -137,16 +164,28 @@ test("arrow, Home, and End keys move focus and activate tabs", async ({ page })
|
||||
await expect(general).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
|
||||
test("programmatic tab changes do not clamp an existing scroll position", async ({ page }) => {
|
||||
test("programmatic tab changes do not clamp an existing scroll position", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSettings(page);
|
||||
await page.evaluate(() => window.scrollTo(0, 240));
|
||||
const before = await layout(page);
|
||||
await page.evaluate(() => document.querySelector<HTMLButtonElement>("#settings-tab-telegram")!.click());
|
||||
await expect.poll(async () => (await layout(page)).scrollY).toBe(before.scrollY);
|
||||
await expect.poll(async () => (await layout(page)).content).toEqual(before.content);
|
||||
await page.evaluate(() =>
|
||||
document
|
||||
.querySelector<HTMLButtonElement>("#settings-tab-telegram")!
|
||||
.click(),
|
||||
);
|
||||
await expect
|
||||
.poll(async () => (await layout(page)).scrollY)
|
||||
.toBe(before.scrollY);
|
||||
await expect
|
||||
.poll(async () => (await layout(page)).content)
|
||||
.toEqual(before.content);
|
||||
});
|
||||
|
||||
test("deleting a newly added account clears its device authorization", async ({ page }) => {
|
||||
test("deleting a newly added account clears its device authorization", async ({
|
||||
page,
|
||||
}) => {
|
||||
let accounts: Array<(typeof responses.accounts)[number]> = [];
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const request = route.request();
|
||||
@@ -183,12 +222,18 @@ test("deleting a newly added account clears its device authorization", async ({
|
||||
await page.getByRole("tab", { name: "Codex" }).click();
|
||||
await page.getByRole("button", { name: "添加账号" }).click();
|
||||
await expect(page.getByText("PLTJ-7M6I6")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "https://auth.openai.com/codex/device" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "https://auth.openai.com/codex/device" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "删除“账号 1”" }).click();
|
||||
await expect(page.getByText("PLTJ-7M6I6")).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "https://auth.openai.com/codex/device" })).toHaveCount(0);
|
||||
await expect(page.getByText("尚未添加 Codex 账号", { exact: false })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "https://auth.openai.com/codex/device" }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByText("尚未添加 Codex 账号", { exact: false }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Codex account cards fit within the viewport", async ({ page }) => {
|
||||
@@ -197,8 +242,11 @@ test("Codex account cards fit within the viewport", async ({ page }) => {
|
||||
const overflow = await page.evaluate(() => ({
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: document.documentElement.clientWidth,
|
||||
cardWidth: document.querySelector(".account-card")!.getBoundingClientRect().width,
|
||||
contentWidth: document.querySelector(".settings-content")!.getBoundingClientRect().width,
|
||||
cardWidth: document.querySelector(".account-card")!.getBoundingClientRect()
|
||||
.width,
|
||||
contentWidth: document
|
||||
.querySelector(".settings-content")!
|
||||
.getBoundingClientRect().width,
|
||||
}));
|
||||
expect(overflow.documentWidth).toBeLessThanOrEqual(overflow.viewportWidth);
|
||||
expect(overflow.cardWidth).toBeLessThanOrEqual(overflow.contentWidth);
|
||||
@@ -222,6 +270,7 @@ test("Codex account emails are masked everywhere they are displayed", async ({
|
||||
limits: [
|
||||
{
|
||||
limitId: "primary",
|
||||
limitName: null,
|
||||
windowType: "5h",
|
||||
usedPercent: 25,
|
||||
windowDurationMinutes: 300,
|
||||
@@ -238,7 +287,9 @@ test("Codex account emails are masked everywhere they are displayed", async ({
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.locator(".account-select")).toContainText("t**t@e*****e.com");
|
||||
await expect(page.locator(".account-select")).toContainText(
|
||||
"t**t@e*****e.com",
|
||||
);
|
||||
await expect(page.locator(".account b")).toContainText("t**t@e*****e.com");
|
||||
await expect(page.getByText("已使用 25%", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("剩余 75%", { exact: true })).toHaveCount(0);
|
||||
@@ -250,6 +301,84 @@ test("Codex account emails are masked everywhere they are displayed", async ({
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("tab", { name: "Codex" }).click();
|
||||
await expect(page.locator(".account-meta").first()).toContainText("t**t@e*****e.com");
|
||||
await expect(page.locator(".account-meta").first()).toContainText(
|
||||
"t**t@e*****e.com",
|
||||
);
|
||||
await expect(page.locator("body")).not.toContainText("test@example.com");
|
||||
});
|
||||
|
||||
test("returns to login when an authenticated request receives 401", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const key = new URL(route.request().url()).pathname.replace("/api/v1/", "");
|
||||
if (key === "accounts/1/sync")
|
||||
return route.fulfill({ status: 401, json: { error: "未登录" } });
|
||||
if (key === "dashboard")
|
||||
return route.fulfill({
|
||||
json: {
|
||||
accountId: 1,
|
||||
displayName: "默认账号",
|
||||
account: { email: null, planType: null, connected: false },
|
||||
limits: [],
|
||||
summary: {},
|
||||
usage: [],
|
||||
fetchedAt: 0,
|
||||
stale: false,
|
||||
},
|
||||
});
|
||||
return route.fulfill({ json: responses[key] ?? {} });
|
||||
});
|
||||
await page.goto("/");
|
||||
await expect(page.locator(".account-select")).toBeVisible();
|
||||
await page.getByRole("button", { name: "立即刷新" }).click();
|
||||
await expect(page.getByRole("heading", { name: "欢迎回来" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("a slow previous account response cannot replace the selected account", async ({
|
||||
page,
|
||||
}) => {
|
||||
const accounts = [
|
||||
responses.accounts[0],
|
||||
{
|
||||
...responses.accounts[0],
|
||||
id: 2,
|
||||
displayName: "第二账号",
|
||||
email: "second@example.com",
|
||||
},
|
||||
];
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const key = requestUrl.pathname.replace("/api/v1/", "");
|
||||
if (key === "accounts") return route.fulfill({ json: accounts });
|
||||
if (key === "dashboard") {
|
||||
const accountId = Number(requestUrl.searchParams.get("accountId"));
|
||||
if (accountId === 1)
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
return route.fulfill({
|
||||
json: {
|
||||
accountId,
|
||||
displayName: accountId === 1 ? "默认账号" : "第二账号",
|
||||
account: {
|
||||
email: accountId === 1 ? "test@example.com" : "second@example.com",
|
||||
planType: "plus",
|
||||
connected: true,
|
||||
},
|
||||
limits: [],
|
||||
summary: {},
|
||||
usage: [],
|
||||
fetchedAt: 0,
|
||||
stale: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fulfill({ json: responses[key] ?? {} });
|
||||
});
|
||||
await page.goto("/");
|
||||
const selector = page.locator(".account-select");
|
||||
await selector.selectOption("2");
|
||||
await expect(page.locator(".account b")).toContainText("第二账号");
|
||||
await page.waitForTimeout(350);
|
||||
await expect(page.locator(".account b")).toContainText("第二账号");
|
||||
await expect(page.locator(".account b")).not.toContainText("默认账号");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user