8000 test(web): update stale browser assertions to the current design by InfinityPacer · Pull Request #13 · InfinityPacer/meterleaf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tests/appearance-sorting.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ try {
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.locator(".app-shell")).toHaveCount(0);
await expect(page.locator("html")).toHaveClass(/dark/);
// React 未加载时由 index.html 的首屏样式着色,深色取 .dark 的 --canvas(#0f0f11)。
await expect(page.locator("html")).toHaveCSS(
"background-color",
"rgb(23, 27, 32)",
"rgb(15, 15, 17)",
);
await page.unroute(mainScript);
await page.reload();
Expand Down
23 changes: 20 additions & 3 deletions tests/mobile-app.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type ViewQuery,
} from "../src/shared/ledger-view";
import { aggregateReport } from "../src/web/lib/report";
import { estimateAmount } from "../src/web/lib/quota-display";
import type { SyncStatus } from "../src/server/sync";

const base = process.env.METERLEAF_TEST_URL ?? "http://127.0.0.1:4331/";
Expand Down Expand Up @@ -107,6 +108,21 @@ const results: unknown[] = [];
const views = ["overview", "reports", "ledger", "settings"] as const;
let screens = 0;

// 演示账本给出 7d 整周预估;Web 账户行按来源顺序为每个有额度窗口的账户显示订阅等价 USD 预估。
const demoLedger = createDemoLedger("subscription");
const expectedWeeklyEstimates = demoLedger.accounts
.filter(
(account) => account.fiveHour || account.sevenDay || account.sevenDayFable,
)
.map((account) => estimateAmount(account.sevenDay, "usd", demoLedger.asOf));
if (
!expectedWeeklyEstimates.length ||
expectedWeeklyEstimates.some((value) => !/^\$[\d,]+\.\d{2}$/.test(value))
)
throw new Error(
`Demo weekly estimates must be amounts: ${expectedWeeklyEstimates}`,
);

const viewRoutePattern = "**/api/view**";
const archiveRoutePattern = "**/api/accounts/archive**";
const syncRoutePattern = "**/api/sync**";
Expand Down Expand Up @@ -391,7 +407,7 @@ async function ready() {
await expect(page.locator(".desktop-period .trend-panel")).toBeVisible();
await expect(
page.locator(".overview-quotas .account-capacity > strong"),
).toHaveText(["N/A", "N/A"]);
).toHaveText(expectedWeeklyEstimates);
await expect(
page
.locator(".overview-quotas .account-capacity")
Expand Down Expand Up @@ -708,11 +724,12 @@ try {
await expec 8000 t(page.getByRole("navigation", { name: "底部导航" })).toBeHidden();
await page.getByRole("button", { name: "打开导航" }).click();
await expect(page.getByRole("dialog")).toBeVisible();
// 抽屉边框取 --color-border,即浅色主题的 --line(#e4e4e7)。
expect(
await page
.getByRole("dialog")
.evaluate((element) => getComputedStyle(element).borderRightColor),
).toBe("rgb(229, 233, 238)");
).toBe("rgb(228, 228, 231)");
await page.screenshot({ path: "test-results/mobile-sidebar.png" });
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: "打开导航" })).toBeFocused();
Expand Down Expand Up @@ -939,7 +956,7 @@ try {
const summary = {
status: "passed",
checks:
"four navigation items, merged overview, overview account management, overview range chips and date picker, range-scoped summary and trend, quotas independent of range, about route, mode persistence, drawer border/focus, overview and report filter sheets, filter focus, detail focus/trap, pagination, reflow, text spacing, reduced motion, line micro trends, N/A estimates, transparent actions, compact filter sheet, overview and account pageSize=1 query bounds",
"four navigation items, merged overview, overview account management, overview range chips and date picker, range-scoped summary and trend, quotas independent of range, about route, mode persistence, drawer border/focus, overview and report filter sheets, filter focus, detail focus/trap, pagination, reflow, text spacing, reduced motion, line micro trends, weekly estimate amounts, transparent actions, compact filter sheet, overview and account pageSize=1 query bounds",
screens,
errors,
a11y: axeSource ? "executed" : "skipped: METERLEAF_AXE_PATH not provided",
Expand Down
178 changes: 116 additions & 62 deletions tests/report-preferences.browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { chromium, expect } from "@playwright/test";
import { chromium, expect, type Request } from "@playwright/test";

const browser = await chromium.connectOverCDP(process.env.METERLEAF_CDP_URL!);
const base = process.env.METERLEAF_TEST_URL ?? "http://127.0.0.1:4321/";
Expand All @@ -7,26 +7,44 @@ const page = browser
.flatMap((context) => context.pages())
.find((page) => page.url().startsWith(base));
if (!page) throw new Error("Existing task page required");
const keys = [
"meterleaf-report-filter",
"meterleaf-usd-basis",
...[
"page",
"unit",
"granularity",
"chart",
"report-dimension",
"record-sort",
"report-sort",
"distribution-unit",
].map((key) => `meterleaf-pref-${key}`),
];
const before = await page.evaluate(
(keys) => keys.map((key) => localStorage.getItem(key)),
keys,

// 每个页面的筛选分别保存在 meterleaf-report-filter-<视图>,总览使用 home 视图;计价口径全局共享。
const filterKey = (scope: "home" | "reports" | "ledger") =>
`meterleaf-report-filter-${scope}`;
const readJson = (key: string) =>
page.evaluate((key) => {
const value = localStorage.getItem(key);
return value === null ? null : JSON.parse(value);
}, key);
const before = await page.evaluate(() =>
Object.fromEntries(
Object.entries(localStorage).filter(([key]) =>
key.startsWith("meterleaf-"),
),
),
);
const beforeViewport = page.viewportSize();
// 主列表请求带分页大小 12;pageSize=1 是摘要与趋势查询。
const isMainView = (request: Request) => {
const url = new URL(request.url());
return (
url.pathname === "/api/view" && url.searchParams.get("pageSize") !== "1"
);
};
async function reloadAndCaptureView(hash: string) {
const request = page!.waitForRequest(isMainView);
await page!.goto(base + "#" + hash);
await page!.reload();
return new URL((await request).url()).searchParams;
}

try {
await page.evaluate(() => localStorage.removeItem("meterleaf-report-filter"));
// 该套检查覆盖 Web 布局下的筛选栏与分组按钮。
await page.setViewportSize({ width: 1440, height: 1000 });
await page.evaluate(() => {
for (const key of Object.keys(localStorage))
if (key.startsWith("meterleaf-")) localStorage.removeItem(key);
});
await page.goto(base + "#reports");
await page.reload();
await page.getByRole("button", { name: "日期范围", exact: true }).click();
Expand All @@ -35,43 +53,70 @@ try {
await page.getByRole("button", { name: "关闭日期选择" }).click();
for (const label of ["模型筛选", "账户筛选"]) {
await expect(page.locator("main")).toHaveAttribute("aria-busy", "false");
await page.getByRole("combobox", { name: label }).click();
await page.getByRole("combobox", { name: label, exact: true }).click();
await expect(page.getByRole("listbox")).toBeVisible();
await page.getByRole("listbox").getByRole("option").nth(1).click();
await expect(page.getByRole("listbox")).toHaveCount(0);
await expect
.poll(() =>
page.evaluate(
(label) =>
JSON.parse(localStorage.getItem("meterleaf-report-filter")!)[
label === "模型筛选" ? "model" : "account"
],
label,
),
)
.poll(async () => {
const saved = await readJson(filterKey("reports"));
return saved?.[label === "模型筛选" ? "model" : "account"] ?? "all";
})
.not.toBe("all");
}
await page.getByRole("button", { name: "标准 API", exact: true }).click();
const saved = await page.evaluate(() =>
JSON.parse(localStorage.getItem("meterleaf-report-filter")!),
);
const saved = await readJson(filterKey("reports"));
expect(saved.dateRange).toEqual({ from: "2026-08-20", to: "2026-08-27" });
expect(saved.model).not.toBe("all");
expect(saved.account).not.toBe("all");
for (const tab of ["overview", "reports", "ledger"]) {
const request = page.waitForRequest(
(request) => new URL(request.url()).pathname === "/api/view",
);
await page.goto(base + "#" + tab);
await page.reload();
const params = new URL((await request).url()).searchParams;
expect(params.get("from")).toBe(saved.dateRange.from);
expect(params.get("to")).toBe(saved.dateRange.to);
expect(params.get("model")).toBe(saved.model);
expect(params.get("account")).toBe(saved.account);
await expect(
page.getByRole("button", { name: "标准 API", exact: true }),
).toHaveAttribute("aria-pressed", "true");
}
await expect
.poll(() =>
page.evaluate(() => localStorage.getItem("meterleaf-usd-basis")),
)
.toBe("api");

// 统计报表重新载入后按保存的范围、模型和账户查询。
const reportParams = await reloadAndCaptureView("reports");
expect(reportParams.get("from")).toBe(saved.dateRange.from);
expect(reportParams.get("to")).toBe(saved.dateRange.to);
expect(reportParams.get("model")).toBe(saved.model);
expect(reportParams.get("account")).toBe(saved.account);
await expect(
page.getByRole("button", { name: "标准 API", exact: true }),
).toHaveAttribute("aria-pressed", "true");

// 请求明细与总览不继承统计报表的筛选,只共享计价口径。
const ledgerParams = await reloadAndCaptureView("ledger");
expect(ledgerParams.get("from")).toBeNull();
expect(ledgerParams.get("days")).toBe("7");
expect(ledgerParams.get("model")).toBe("all");
expect(ledgerParams.get("account")).toBe("all");
await expect(
page.getByRole("button", { name: "标准 API", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await page.goto(base + "#overview");
await page.reload();
await expect(
page.getByRole("button", { name: "日期范围", exact: true }),
).toContainText("历史至今");
await expect(
page.getByRole("button", { name: "标准 API", exact: true }),
).toHaveAttribute("aria-pressed", "true");
expect(await readJson(filterKey("home"))).toBeNull();

// 请求明细自己的筛选同样持久化;搜索词与页码不保存;清除只重置当前页面。
await page.goto(base + "#ledger");
await page.reload();
await expect(page.locator("main")).toHaveAttribute("aria-busy", "false");
await page.getByRole("combobox", { name: "模型筛选", exact: true }).click();
await page.getByRole("listbox").getByRole("option").nth(1).click();
await expect(page.getByRole("listbox")).toHaveCount(0);
await expect
.poll(async () => (await readJson(filterKey("ledger")))?.model ?? "all")
.not.toBe("all");
const ledgerModel = (await readJson(filterKey("ledger"))).model;
const ledgerReloaded = await reloadAndCaptureView("ledger");
expect(ledgerReloaded.get("model")).toBe(ledgerModel);
await page
.getByRole("textbox", { name: "搜索请求", exact: true })
.fill("not-persisted-search");
Expand All @@ -81,24 +126,31 @@ try {
).toHaveValue("");
await page.getByRole("button", { name: "清除筛选", exact: true }).click();
await page.reload();
const reset = await page.evaluate(() =>
JSON.parse(localStorage.getItem("meterleaf-report-filter")!),
);
expect(reset).toEqual({ days: 7, model: "all", account: "all" });
expect(await readJson(filterKey("ledger"))).toEqual({
days: 7,
model: "all",
account: "all",
});
expect(await readJson(filterKey("reports"))).toEqual(saved);
await page.getByRole("button", { name: "下一页", exact: true }).click();
await expect(page.locator(".pagination")).toContainText("第 2 /");
await page.reload();
await expect(page.locator(".pagination")).toContainText("第 1 /");

// 总览的图表单位默认 Tokens,改为 USD 后与图表样式、粒度一起按总览保存。
await page.goto(base + "#overview");
await page.getByRole("button", { name: "Tokens", exact: true }).click();
await expect(
page.getByRole("button", { name: "Tokens", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await page.getByRole("button", { name: "USD", exact: true }).click();
await page.getByRole("button", { name: "折线图", exact: true }).click();
await page
.getByRole("group", { name: "时间粒度", exact: true })
.getByRole("button", { name: "周", exact: true })
.click();
await page.reload();
await expect(
page.getByRole("button", { name: "Tokens", exact: true }),
page.getByRole("button", { name: "USD", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await expect(
page.getByRole("button", { name: "折线图", exact: true }),
Expand All @@ -108,6 +160,9 @@ try {
.getByRole("group", { name: "时间粒度", exact: true })
.getByRole("button", { name: "周", exact: true }),
).toHaveAttribute("aria-pressed", "true");
expect(await readJson("meterleaf-pref-home-unit")).toBe("usd");
expect(await readJson("meterleaf-pref-home-chart")).toBe("line");
expect(await readJson("meterleaf-pref-home-granularity")).toBe("week");
await page.goto(base + "#reports");
await page
.getByRole("group", { name: "汇总维度" })
Expand Down Expand Up @@ -136,6 +191,7 @@ try {
dates: true,
model: true,
account: true,
perViewFilters: true,
usdBasis: true,
chart: true,
unit: true,
Expand All @@ -149,15 +205,13 @@ try {
}),
);
} finally {
await page.evaluate(
({ keys, before }) =>
keys.forEach((key, i) =>
before[i] === null
? localStorage.removeItem(key)
: localStorage.setItem(key, before[i]!),
),
{ keys, before },
);
await page.evaluate((before) => {
for (const key of Object.keys(localStorage))
if (key.startsWith("meterleaf-")) localStorage.removeItem(key);
for (const [key, value] of Object.entries(before))
localStorage.setItem(key, value);
}, before);
if (beforeViewport) await page.setViewportSize(beforeViewport);
await page.reload();
await browser.close();
}
12 changes: 12 additions & 0 deletions tests/sync-refresh.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ try {
// 桌面外壳在品牌与主导航之间保留 24px(desktop.css)。
expect(gap).toBe(24);
}
const appLayout = await page.evaluate(
() =>
innerWidth <= 900 &&
document.documentElement.dataset.mobileLayout === "app",
);
if (appLayout) {
// App 布局只在首页顶栏放同步入口,其他页面隐藏(mobile.css 的 .app-topbar:not(.app-home-topbar) .app-sync)。
await expect(
page.getByRole("button", { name: "数据同步", exact: true }),
).toBeHidden();
await page.goto(base + "#overview");
}
await page.getByRole("button", { name: "数据同步", exact: true }).click();
await expect(page.getByText("自动同步", { exact: true })).toBeVisible();
await expect(page.getByText("自动刷新", { exact: true })).toHaveCount(0);
Expand Down
Loading
Loading
0