8000 test(browser): bring the dedicated browser scripts back in line with the app by InfinityPacer · Pull Request #66 · 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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Meterleaf 是独立 AI 用量账本,只读采集上游用量并在本地计价
- 单元与集成测试使用 Bun,命名为 `tests/*.test.ts`;浏览器测试使用 Playwright,专项脚本通常命名为 `*.browser.ts`。
- 修复问题时覆盖对应回归场景;计价、迁移和同步变更需验证未知值、幂等与失败恢复,不以虚构覆盖率作为门槛。
- 界面改动检查桌面、移动端和减少动态效果模式;`bun run test:browser` 通过 `METERLEAF_CDP_URL` 连接任务浏览器。截图放入已忽略的 `test-results`,视觉验收由维护者确认。
- 专项 `*.browser.ts` 脚本读写真实接口,先用 `METERLEAF_DEMO=true bun --no-env-file src/server/main.ts` 起演示后端,再用 `bun run dev` 起页面,`METERLEAF_TEST_URL` 指向该页面的任务标签;`quota-estimate` 自带临时服务,只需要 `METERLEAF_CDP_URL`。
- 浏览器测试使用独立任务标签;修改 localStorage 时与维护者预览页隔离 origin。CDP 连接放在有 `finally` 清理的脚本内,结束时撤销模拟并断开连接,不在长期 REPL 中遗留 Playwright 客户端;系统主题须在清理后再次刷新验证。
- 高保真参考与维护者已认可版本是视觉依据,修复前保留其可恢复位置和已知缺陷。代理预审按当前任务要求校对参考、导航和受影响状态,明确偏差先处理;预审通过与维护者最终接受分别记录。维护者接管验收时交出可访问版本和已知限制。
- 纯文档改动核对对应事实、相对链接、所改 Markdown 的 Prettier 检查与 `git diff --check`,不触发产品完整测试。
Expand Down
11 changes: 8 additions & 3 deletions tests/mobile-app.browser.ts
Original file line number Diff line number Diff line change
8000 Expand Up @@ -627,11 +627,16 @@ async function assertMobileAccountManagement() {
key: "ArrowUp" | "ArrowDown",
) => {
const handle = cards.nth(index).getByRole("button", { name: /^拖动排序 / });
// 拖放库播报拿起之后才开始监听方向键,紧跟着按下的方向键会被忽略。被忽略的按键不移动账户,
// 所以没等到移动播报就重按,直到读屏播报确认移动后再放下。
const announcement = page.locator('[id^="DndLiveRegion-"]');
await handle.focus();
await page.keyboard.press("Space");
await page.waitForTimeout(30);
await page.keyboard.press(key);
await page.waitForTimeout(30);
await expect(announcement).toContainText("已拿起");
await expect(async () => {
await page.keyboard.press(key);
await expect(announcement).toContainText("移到", { timeout: 1000 });
}).toPass({ timeout: 10_000 });
await page.keyboard.press("Space");
};
await section
Expand Down
6 changes: 3 additions & 3 deletions tests/overview-account-usage.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ try {
.locator(".overview-quotas .account-row")
.filter({ hasText: apiAccount.name });
await expect(apiRow).toContainText("时段 Tokens");
await expect(apiRow).toContainText("8.4K");
await expect(apiRow).toContainText("8.40K");
const subscriptionRow = page
.locator(".overview-quotas .account-row")
.filter({ hasText: subscriptionAccount.name });
Expand All @@ -89,12 +89,12 @@ try {
).toBeVisible();
await expect(
subscriptionRow.locator(".quota-window-volume").first(),
).toHaveText("129.6K Tokens · 12 次");
).toHaveText("129.60K Tokens · 12 次");

withLifetime = true;
await page.reload();
await expect(apiRow).toContainText("累计 Tokens");
await expect(apiRow).toContainText("50M");
await expect(apiRow).toContainText("50.00M");

for (const width of [1440, 1200, 1050, 390]) {
await page.setViewportSize({ width, height: 1000 });
Expand Down
92 changes: 71 additions & 21 deletions tests/quota-estimate.browser.ts
8000
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { tmpdir } from "node:os";
import { resolve } from "node:path";
import type { IngestBatch } from "../src/shared/ingest";
import type { LedgerView } from "../src/shared/ledger-view";
import { fullWeekEstimate } from "./quota-projection";

const endpoint = process.env.METERLEAF_CDP_URL;
if (!endpoint)
Expand Down Expand Up @@ -185,8 +186,9 @@ async function apiStage(
.toBe(true);
const history = accountWindow(view!, "history");
const rough = accountWindow(view!, "rough");
expect(Number(history.estimate!.usd)).toBeCloseTo(usd, 8);
expect(Number(rough.estimate!.usd)).toBeCloseTo(roughUsd, 8);
// 预估是已花加剩余比例乘整周外推值,这里核对外推值本身。
expect(Number(fullWeekEstimate(history))).toBeCloseTo(usd, 8);
expect(Number(fullWeekEstimate(rough))).toBeCloseTo(roughUsd, 8);
expect(history.estimate!.reason).toBe("eligible");
expect(rough.estimate!.reason).toBe("eligible");
expect(Number(history.periodUsd)).toBe(expectedPeriodUsd);
Expand All @@ -204,6 +206,11 @@ async function apiStage(
);
console.log(JSON.stringify({ method, usd, roughMethod, roughUsd }));
}
/** 页面显示的预估:已花加剩余比例乘整周外推值。 */
const shown = (spent: number, percent: number, fullWeek: number) =>
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(
spent + (fullWeek * (100 - percent)) / 100,
);
async function detail(
account: keyof typeof names,
amount: string,
Expand Down Expand Up @@ -290,18 +297,31 @@ try {
localStorage.setItem("meterleaf-pref-mobileLayout", JSON.stringify("app")),
);
await page.reload();
await detail("history", "$100.00", /上一周期/, "desktop-previous-period.png");
await detail("rough", "$800.00", /粗估/);
await detail(
"history",
shown(8, 1, 100),
/上一周期/,
"desktop-previous-period.png",
);
await detail("rough", shown(8, 1, 800), /粗估/);
for (const account of ["history", "rough"] as const) {
await ingest(observation(account, 2, 16, 8));
await ingest(observation(account, 3, 24, 16));
}
// 当前粗估即使为 800 USD,有历史参考时也继续采用 100 USD。
await apiStage("previous-period", 100, "rough", 800, 24);
// 上游百分比四舍五入,2% 与 3% 按首次到达记为 1.5% 与 2.5%,过原点加权粗估为
// 100 × (1×8 + 1.5×16 + 2.5×24) / (1² + 1.5² + 2.5²) = 18400/19 USD。
const earlyRough = 18400 / 19;
// 当前粗估即使远高于历史,有历史参考时也继续采用 100 USD。
await apiStage("previous-period", 100, "rough", earlyRough, 24);
await loadPage();
await tokensCard("2.16M", "9.00K");
await detail("history", "$100.00", /上一周期/, "desktop-early-history.png");
await detail("rough", "$800.00", /粗估/);
await detail(
"history",
shown(24, 3, 100),
/上一周期/,
"desktop-early-history.png",
);
await detail("rough", shown(24, 3, earlyRough), /粗估/);
await page.setViewportSize({ width: 390, height: 844 });
await page.emulateMedia({ reducedMotion: "reduce" });
await loadPage();
Expand All @@ -311,20 +331,26 @@ try {
),
).toBe(true);
await tokensCard("2.16M", "9.00K");
await detail("history", "$100.00", /上一周期/, "mobile-early-history.png");
await detail("rough", "$800.00", /粗估/, "mobile-rough.png");
await detail(
"history",
shown(24, 3, 100),
/上一周期/,
"mobile-early-history.png",
);
await detail("rough", shown(24, 3, earlyRough), /粗估/, "mobile-rough.png");
for (const account of ["history", "rough"] as const) {
await ingest(observation(account, 10, 30, 24));
await ingest(observation(account, 15, 35, 30));
}
// 七个有效点对的中位数为 1900/13,跨度 14% 的历史混合权重为 0.49。
const blended = 100 * 0.51 + (1900 / 13) * 0.49;
await apiStage("blended", blended, "segments", 1900 / 13, 35);
// 2% 记为 1.5% 后,七个有效点对的中位数是 1.5% 到 15% 这一对,即 100 × 19 / 13.5 = 3800/27,
// 跨度 14% 的本周期权重为 0.49。
const blended = 100 * 0.51 + (3800 / 27) * 0.49;
await apiStage("blended", blended, "segments", 3800 / 27, 35);
await loadPage();
await tokensCard("1.92M", "13.00K");
await detail(
"history",
"$122.62",
shown(35, 15, blended),
/综合上一周期与本周期/,
"mobile-blended.png",
);
Expand All @@ -333,7 +359,7 @@ try {
await tokensCard("1.92M", "13.00K");
await detail(
"history",
"$122.62",
shown(35, 15, blended),
/综合上一周期与本周期/,
"desktop-blended.png",
);
Expand All @@ -347,11 +373,21 @@ try {
await apiStage("segments", 100, "segments", 100, 80);
await page.setViewportSize({ width: 390, height: 844 });
await loadPage();
await detail("history", "$100.00", /多段.*中位数/, "mobile-segments.png");
await detail("rough", "$100.00", /多段.*中位数/);
await detail(
"history",
shown(80, 60, 100),
/多段.*中位数/,
"mobile-segments.png",
);
await detail("rough", shown(80, 60, 100), /多段.*中位数/);
await page.setViewportSize({ width: 1440, height: 1000 });
await loadPage();
await detail("history", "$100.00", /多段.*中位数/, "desktop-segments.png");
await detail(
"history",
shown(80, 60, 100),
/多段.*中位数/,
"desktop-segments.png",
);
// 套餐变化时百分比仍递增,验证真正隔离套餐,而非仅靠百分比回退。
const changed = (percent: number, addedUsd: number) => {
const batch = observation("history", percent, addedUsd, 0);
Expand All @@ -371,7 +407,11 @@ try {
result = (await response.json()) as LedgerView;
const window = accountWindow(result, "history");
return (
window.percent === percent && window.estimate?.usd === expected
window.percent === percent &&
(fullWeekEstimate(window) === null
? null
: Number(fullWeekEstimate(window))) ===
(expected === null ? null : Number(expected))
);
},
{ timeout: 15000 },
Expand All @@ -393,10 +433,20 @@ try {
"rough",
);
await loadPage();
await detail("history", "$200.00", /粗估/, "desktop-plan-changed.png");
await detail(
"history",
shown(86, 64, 200),
/粗估/,
"desktop-plan-changed.png",
);
await page.setViewportSize({ width: 390, height: 844 });
await loadPage();
await detail("history", "$200.00", /粗估/, "mobile-plan-changed.png");
await detail(
"history",
shown(86, 64, 200),
/粗估/,
"mobile-plan-changed.png",
);
expect(errors).toEqual([]);
const result = {
status: "passed",
Expand Down
4 changes: 4 additions & 0 deletions tests/quota-pwa-layout.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ await page.route("**/api/**", async (route) => {
priceVersion: "fixture",
};
await route.fulfill({ json: result });
} else if (url.pathname === "/api/accounts/merge") {
await route.fulfill({
json: { writable: false, accounts: [], links: [] },
});
} else if (url.pathname.includes("archive")) {
await route.fulfill({
json: { archived: [], hidden: [], writable: false },
Expand Down
Loading
0