Files
ai-directory-platform/packages/core/scripts/ui-test-admin.mjs
T
bigcode 715152f159 fix: 登录态快速点击被误判未登录踢到登录页 + 乐观计数闪烁; 新增三套浏览器 GUI 测试脚本
- 投票/收藏/评论入口在 session.isPending(会话解析中)时不再把
  已登录用户误判为未登录跳转登录页(刷新后快速点击可复现)
- 工具详情页 VotesProvider 传入 initialCounts,消除投票后计数闪现 1 的回退
- 新增 packages/core/scripts/ui-test-{user,admin,cross-site}.mjs:
  用户全流程 24 项 / 管理员后台 9 项 / 跨站抽查 20 项,实测全部通过
2026-09-16 06:10:41 +08:00

253 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 ui-test-admin.mjs
* 说明:管理员后台 GUI 测试(Playwright + 系统 Brave)。
* 覆盖:admin 登录、后台面板、提交审批(Approve → 工具上线验证 →
* 后台删除工具)、拒绝流(Reject 清理其余 E2E 提交)、认领审批、
* 评论隐藏/恢复开关、用户列表。
* 运行:cd packages/core && bun scripts/ui-test-admin.mjs [baseURL]
*/
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const BASE = (process.argv[2] ?? "http://localhost:3001").replace(/\/$/, "");
const SHOT_DIR = "/tmp/ui-test";
mkdirSync(SHOT_DIR, { recursive: true });
const results = [];
const step = async (name, fn) => {
try {
await fn();
results.push(["PASS", name]);
console.log(`PASS ${name}`);
} catch (e) {
results.push(["FAIL", name]);
console.log(`FAIL ${name}\n ${String(e).split("\n")[0]}`);
}
};
const shot = (page, name) =>
page.screenshot({ path: `${SHOT_DIR}/${name}.png`, fullPage: false });
const gotoWait = async (p, url) => {
await p.goto(url, { waitUntil: "domcontentloaded" });
await p.waitForLoadState("networkidle", { timeout: 30000 }).catch(() => {});
await p.waitForTimeout(800);
};
/* 重试直到 check() 通过:admin 页水合完成前点击会被吞,
每次重试间隔 2s,最多 attempts 次 */
const retryUntil = async (p, act, check, attempts = 4) => {
for (let i = 0; i < attempts; i++) {
await act();
try {
await check();
return;
} catch (e) {
if (i === attempts - 1) throw e;
}
await p.waitForTimeout(2000);
}
};
const browser = await chromium.launch({
executablePath: "/opt/brave.com/brave/brave",
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
const page = await (
await browser.newContext({ viewport: { width: 1440, height: 900 } })
).newPage();
page.setDefaultTimeout(30000);
// window.confirm 自动接受(后台删除工具用)
page.on("dialog", (d) => d.accept());
/* ---------- 1. 管理员登录 ---------- */
await step("admin 登录成功", async () => {
await gotoWait(page, `${BASE}/login`);
await page.getByLabel(/email/i).fill("admin@aidirectory.dev");
// 安全审计时已轮换的管理员密码(非种子密码 password123)
await page.getByLabel(/password/i).fill("PRmeSJeyr73x1JjW-1Aa1!");
await page.getByRole("button", { name: /sign in/i }).first().click();
await page.waitForURL((u) => !u.pathname.includes("/login"), {
timeout: 20000,
});
});
await step("/admin 后台面板渲染(侧栏 + 统计)", async () => {
await gotoWait(page, `${BASE}/admin`);
await page.getByText("Admin Panel").first().waitFor();
await shot(page, "20-admin-dashboard");
});
/* ---------- 2. 提交审批:批准最新 E2E 提交并验证上线 ---------- */
let approvedSlug = "";
await step("审批提交:Approve 最新 E2E 提交", async () => {
await gotoWait(page, `${BASE}/admin/submissions`);
const cards = () =>
page
.locator("div.rounded-xl.border")
.filter({ hasText: "E2E Test Tool" })
.filter({ has: page.getByRole("button", { name: "Approve" }) });
if ((await cards().count()) === 0) throw new Error("未找到待审 E2E 提交卡片");
const card = cards().first();
approvedSlug = (await card.locator("h3").first().innerText())
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
console.log(` 批准: ${approvedSlug}`);
await retryUntil(
page,
() => card.getByRole("button", { name: "Approve" }).click(),
() => page.getByText(/Approved and published/i).first().waitFor({ timeout: 4000 })
);
await shot(page, "22-admin-submission-approved");
});
await step("审批后工具已上线(前台详情页可访问)", async () => {
await gotoWait(page, `${BASE}/tools/${approvedSlug}`);
const h1 = await page.getByRole("heading", { level: 1 }).first().innerText();
if (!/E2E Test Tool/i.test(h1)) throw new Error(`详情页标题异常: ${h1}`);
await shot(page, "23-e2e-tool-live");
});
/* ---------- 3. 拒绝流:清理其余 E2E 提交 ---------- */
await step("拒绝流:Reject 其余 E2E 提交(附管理员备注)", async () => {
await gotoWait(page, `${BASE}/admin/submissions`); // 重新加载,避开批准后的重渲染
let rejected = 0;
for (let i = 0; i < 20; i++) {
const cards = page
.locator("div.rounded-xl.border")
.filter({ hasText: "E2E Test Tool" })
.filter({ has: page.getByRole("button", { name: "Approve" }) });
if ((await cards.count()) === 0) break;
const card = cards.first();
await retryUntil(
page,
async () => {
await card.getByRole("button", { name: "Reject" }).click();
const note = page.getByPlaceholder(/Reason for rejection/i);
await note.waitFor({ timeout: 8000 });
await note.fill("E2E 测试数据清理");
await page.getByRole("button", { name: /Confirm reject/i }).click();
},
() =>
page
.locator("div.rounded-xl.border")
.filter({ hasText: "E2E Test Tool" })
.filter({ has: page.getByRole("button", { name: "Approve" }) })
.first()
.waitFor({ state: "detached", timeout: 4000 })
.catch(async (e) => {
// 等待的第一条卡片可能已被处理(列表前移),改为校验总数下降
const n = await page
.locator("div.rounded-xl.border")
.filter({ hasText: "E2E Test Tool" })
.filter({ has: page.getByRole("button", { name: "Approve" }) })
.count();
if (n >= 20 - rejected) throw e;
})
);
rejected++;
await page.waitForTimeout(800);
}
console.log(` 共拒绝 ${rejected} 条`);
if (rejected === 0) throw new Error("一条都没有拒绝(预期应清理多条)");
await shot(page, "24-admin-submissions-rejected");
});
/* ---------- 4. 后台删除已上线的 E2E 工具 ---------- */
await step("后台删除 E2E 工具(含确认弹窗)", async () => {
await gotoWait(page, `${BASE}/admin/tools`);
await page.getByRole("heading", { name: /Tools/i }).first().waitFor();
const search = page.getByPlaceholder(/Search tool/i);
if (await search.count()) await search.fill("E2E Test Tool");
await page.waitForTimeout(1000);
const row = () => page.locator("tr", { hasText: "E2E Test Tool" }).first();
if ((await row().count()) === 0) throw new Error("后台工具列表未找到 E2E 工具");
await retryUntil(
page,
() => row().getByRole("button", { name: /^Delete/i }).first().click(),
async () => {
await page.waitForTimeout(1000);
if ((await row().count()) > 0) throw new Error("行仍在");
}
);
await shot(page, "25-admin-tool-deleted");
// 删除后详情页应 404
await gotoWait(page, `${BASE}/tools/${approvedSlug}`);
const body = await page.locator("body").innerText();
if (/E2E Test Tool/i.test(body) && !/not found|404/i.test(body))
throw new Error("工具删除后详情页仍可访问");
});
/* ---------- 5. 认领审批(幂等:已批准则视为通过) ---------- */
await step("认领审批:Approve alice 对 ChatGPT 的认领", async () => {
await gotoWait(page, `${BASE}/admin/claims`);
const card = page
.locator("div.rounded-xl.border")
.filter({ hasText: "Alice Chen" })
.filter({ has: page.getByRole("button", { name: "Approve" }) })
.first();
if ((await card.count()) === 0) {
console.log(" (无待审认领,可能此前已批准)");
} else {
await retryUntil(
page,
() => card.getByRole("button", { name: "Approve" }).click(),
() =>
page
.locator("div.rounded-xl.border")
.filter({ hasText: "Alice Chen" })
.filter({ has: page.getByRole("button", { name: "Approve" }) })
.first()
.waitFor({ state: "detached", timeout: 4000 })
);
}
await shot(page, "26-admin-claim-approved");
});
/* ---------- 6. 评论管理开关 ---------- */
await step("评论管理:Hide → Restore 一条评论", async () => {
await gotoWait(page, `${BASE}/admin/reviews`);
const card = () =>
page
.locator("div.rounded-xl.border")
.filter({ has: page.getByRole("button", { name: "Hide" }) })
.first();
if ((await card().count()) === 0) throw new Error("未找到可隐藏的评论");
const content = (await card().locator("p").last().innerText()).slice(0, 20);
await retryUntil(
page,
() => card().getByRole("button", { name: "Hide" }).click(),
() => page.getByText(/Review hidden/i).first().waitFor({ timeout: 4000 })
);
// 状态徽标变 hidden,按钮变 Restore
const hiddenCard = page
.locator("div.rounded-xl.border")
.filter({ hasText: content })
.filter({ has: page.getByRole("button", { name: "Restore" }) })
.first();
await hiddenCard.waitFor({ timeout: 15000 });
await retryUntil(
page,
() => hiddenCard.getByRole("button", { name: "Restore" }).click(),
() => page.getByText(/Review restored/i).first().waitFor({ timeout: 4000 })
);
await shot(page, "27-admin-review-toggled");
});
/* ---------- 7. 用户列表 ---------- */
await step("用户列表渲染(含演示用户)", async () => {
await gotoWait(page, `${BASE}/admin/users`);
const body = await page.locator("body").innerText();
if (!body.includes("alice@demo.dev")) throw new Error("用户列表缺 alice");
await shot(page, "28-admin-users");
});
await browser.close();
const fails = results.filter(([s]) => s === "FAIL");
console.log(
`\n===== 管理员后台测试(${BASE}):${results.length - fails.length}/${results.length} 通过 =====`
);
if (fails.length) process.exit(1);