fix: 登录态快速点击被误判未登录踢到登录页 + 乐观计数闪烁; 新增三套浏览器 GUI 测试脚本

- 投票/收藏/评论入口在 session.isPending(会话解析中)时不再把
  已登录用户误判为未登录跳转登录页(刷新后快速点击可复现)
- 工具详情页 VotesProvider 传入 initialCounts,消除投票后计数闪现 1 的回退
- 新增 packages/core/scripts/ui-test-{user,admin,cross-site}.mjs:
  用户全流程 24 项 / 管理员后台 9 项 / 跨站抽查 20 项,实测全部通过
This commit is contained in:
bigcode
2026-09-16 06:10:41 +08:00
parent 3f87153ce3
commit 715152f159
7 changed files with 898 additions and 3 deletions
@@ -0,0 +1,174 @@
/**
* # 脚本 ui-test-cross-site.mjs
* 说明:五站跨站抽查(同一底座、同一数据库,核心链路每站验证)。
* 每站:登录 alice → 打开指定工具页 → 发评论再删除(无残留)→
* 投票再取消(恢复原状)→ 登出。
* 运行:cd packages/core && bun scripts/ui-test-cross-site.mjs
*/
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const SHOT_DIR = "/tmp/ui-test";
mkdirSync(SHOT_DIR, { recursive: true });
/* 每站抽查的工具 slug */
const SITES = [
{ name: "launchai", base: "https://launchai-bdev.opentest.cc", slug: "/tools/perplexity" },
{ name: "the-ai-ledger", base: "https://the-ai-ledger-bdev.opentest.cc", slug: "/tools/midjourney" },
{ name: "devai-tools", base: "https://devai-tools-bdev.opentest.cc", slug: "/tools/github-copilot" },
{ name: "findai", base: "https://findai-bdev.opentest.cc", slug: "/tools/figma-ai" },
];
const results = [];
const step = (site, name) => async (fn) => {
try {
await fn();
results.push(["PASS", `${site} ${name}`]);
console.log(`PASS [${site}] ${name}`);
} catch (e) {
results.push(["FAIL", `${site} ${name}`]);
console.log(`FAIL [${site}] ${name}\n ${String(e).split("\n")[0]}`);
}
};
const browser = await chromium.launch({
executablePath: "/opt/brave.com/brave/brave",
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
/* 稳定交互辅助(同用户侧脚本) */
const waitText = async (p, loc, re, timeoutMs = 20000) => {
const deadline = Date.now() + timeoutMs;
let last = "";
while (Date.now() < deadline) {
last = await loc
.innerText()
.catch((e) => `(err:${String(e).split("\n")[0].slice(0, 60)})`);
if (re.test(last)) return last;
await p.waitForTimeout(300);
}
throw new Error(`等待超时(期望 ${re}),当前: ${JSON.stringify(last)}`);
};
const waitActionSettled = (p) =>
p
.waitForLoadState("networkidle", { timeout: 15000 })
.catch(() => {});
const clickToggle = async (p, btn, re, attempts = 3) => {
for (let i = 0; i < attempts; i++) {
await btn.click();
try {
const text = await waitText(p, btn, re, 2500);
await waitActionSettled(p);
return text;
} catch (e) {
if (i === attempts - 1) throw e;
}
}
};
const gotoHydrated = async (p, url) => {
const marker = p
.waitForResponse(
(r) =>
r.request().method() === "POST" &&
!r.url().includes("cdn-cgi") &&
!r.url().includes("/api/auth/"),
{ timeout: 30000 }
)
.catch(() => null);
await p.goto(url, { waitUntil: "domcontentloaded" });
await marker;
await p.waitForTimeout(500);
};
for (const { name: site, base, slug } of SITES) {
const s = (name, fn) => step(site, name)(fn);
const page = await (
await browser.newContext({ viewport: { width: 1440, height: 900 } })
).newPage();
page.setDefaultTimeout(30000);
const shot = (n) => page.screenshot({ path: `${SHOT_DIR}/cs-${site}-${n}.png` });
const mainVoteBtn = () =>
page.locator('button:has-text("Upvote"), button:has-text("Upvoted")').first();
await s("登录 alice", async () => {
await gotoHydrated(page, `${base}/login`).catch(() => {});
await page.goto(`${base}/login`, { waitUntil: "domcontentloaded" });
await page.waitForLoadState("networkidle", { timeout: 30000 }).catch(() => {});
await page.waitForTimeout(500);
await page.getByLabel(/email/i).fill("alice@demo.dev");
await page.getByLabel(/password/i).fill("password123");
await page.getByRole("button", { name: /sign in/i }).first().click();
await page.waitForURL((u) => !u.pathname.includes("/login"), {
timeout: 20000,
});
});
await s(`打开 ${slug} 并水合`, async () => {
await gotoHydrated(page, `${base}${slug}`);
await page.getByRole("heading", { level: 1 }).first().waitFor();
});
await s("发评论 → 删除(无残留)", async () => {
await page
.getByRole("button", { name: /Write a review/i })
.first()
.click();
const group = page.getByRole("radiogroup", { name: "rating" });
await group.waitFor();
await group.getByRole("button", { name: "5" }).click();
await page
.getByPlaceholder(/What did you use it for/i)
.fill(`跨站抽查评论(${site}):验证各站点评论功能,随后删除。`);
await page.getByRole("button", { name: /Publish review/i }).click();
await page.getByText(/跨站抽查评论/).first().waitFor();
await shot("review-created");
// 删除
await page
.getByRole("button", { name: /Write a review/i })
.first()
.click();
await page.getByRole("button", { name: /Update review/i }).waitFor();
await page.getByRole("button", { name: /Delete my review/i }).click();
await page
.getByText(/Review deleted|No reviews yet/)
.first()
.waitFor({ timeout: 20000 });
await page.waitForTimeout(1500);
const body = await page.locator("body").innerText();
if (body.includes("跨站抽查评论")) throw new Error("评论删除后仍显示");
await shot("review-deleted");
});
await s("投票 → 取消(恢复原状)", async () => {
const btn = mainVoteBtn();
await btn.waitFor();
let base0;
if (/Upvoted/i.test(await btn.innerText())) {
await clickToggle(page, btn, /^Upvote ·/);
}
base0 = parseInt((await btn.innerText()).replace(/\D+/g, ""), 10) || 0;
await clickToggle(page, btn, /Upvoted ·/);
await clickToggle(page, btn, /^Upvote ·/);
const after = parseInt((await btn.innerText()).replace(/\D+/g, ""), 10) || 0;
if (after !== base0) throw new Error(`计数未恢复: ${base0} → ${after}`);
await shot("vote-toggled");
});
await s("登出", async () => {
await page.goto(`${base}/dashboard`, { waitUntil: "domcontentloaded" });
await page.getByText("Welcome back").waitFor();
await page.getByRole("button", { name: /Sign Out/i }).click();
await page.waitForURL((u) => !u.pathname.includes("/dashboard"));
});
await page.close();
}
await browser.close();
const fails = results.filter(([s]) => s === "FAIL");
console.log(
`\n===== 跨站抽查:${results.length - fails.length}/${results.length} 通过 =====`
);
if (fails.length) process.exit(1);