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:
@@ -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);
|
||||
Reference in New Issue
Block a user