/** * 知识列表翻页的界面验收:点「加载更多」能把 84 条翻到底,不出错。 * * 这条脚本是有来历的:接口的游标字段名写错(`cur.item_id`),第二页直接 500, * 但库小于一页时永远碰不到——导入 84 条旧备忘之后用户一点就撞上了。 * * 用法: * node web/e2e/knowledge-paging.mjs # 默认打生产 * WEB=http://127.0.0.1:5180 PHONE=13900000001 node web/e2e/knowledge-paging.mjs */ import { readFile } from "node:fs/promises"; import { chromium } from "playwright-core"; const WEB = process.env.WEB ?? "http://192.168.8.188:8200"; const API = process.env.API ?? `${WEB}/api/v1`; const PHONE = process.env.PHONE ?? "13605513683"; const CODE_PIN = process.env.CODE_PIN ?? "123456"; // 生产 pin 就是验证码本身 const CHROME = process.env.CHROME ?? `${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`; let pass = 0; let fail = 0; function check(name, ok, extra = "") { if (ok) { pass += 1; console.log(`PASS ${name}`); } else { fail += 1; console.log(`FAIL ${name} ${extra}`); } } /** * 登录:默认用密码(生产的短信登录已经关了),密码从 PASSWORD 拿, * 或者从 .local/e2e-account-password.txt 读(dev 13900000001 的密码存在那儿)。 * 真没密码时才回落到短信验证码——那种环境下短信登录得开着。 */ async function login() { const password = await readPassword(); if (password) { const r = await fetch(`${API}/auth/login/password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: PHONE, password }), }); const body = await r.json(); if (body.code !== 0) throw new Error(`登录失败:${body.message}`); return { tokens: body.data.tokens, workspaces: await workspaces(body.data.tokens) }; } const sent = await fetch(`${API}/auth/sms/send`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: PHONE }), }).then((r) => r.json()); if (sent.code !== 0) throw new Error(`发码失败:${sent.message}(配个 PASSWORD 或写下密码文件就能走密码登录)`); const login = await fetch(`${API}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: PHONE, code: CODE_PIN }), }); const body = await login.json(); if (body.code !== 0) throw new Error(`登录失败:${body.message}`); return { tokens: body.data.tokens, workspaces: await workspaces(body.data.tokens) }; } async function readPassword() { if (process.env.PASSWORD) return process.env.PASSWORD; try { const file = new URL("../../.local/e2e-account-password.txt", import.meta.url); const text = (await readFile(file, "utf8")).trim(); return text || null; } catch { return null; } } async function workspaces(tokens) { const r = await fetch(`${API}/workspaces`, { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); const body = await r.json(); return body.data?.items ?? body.data ?? []; } async function main() { const { tokens, workspaces: spaces } = await login(); if (!spaces.length) throw new Error("这个账号没有工作区"); const ws = spaces[0]; // 先问接口一共有多少条,作为界面上应该数出来的数字 const head = await fetch(`${API}/workspaces/${ws.id}/knowledge?limit=100`, { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); const headBody = await head.json(); const total = (headBody.data?.items ?? []).length; const browser = await chromium.launch({ executablePath: CHROME }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); await context.addInitScript( ([access, refresh]) => { localStorage.setItem("dadao.access", access); localStorage.setItem("dadao.refresh", refresh); }, [tokens.access_token, tokens.refresh_token], ); const page = await context.newPage(); const errors = []; const bad = []; page.on("pageerror", (e) => errors.push(String(e))); page.on("response", (r) => { if (r.url().includes("/knowledge?") && r.status() >= 400) bad.push(`${r.status()} ${r.url()}`); }); await page.goto(`${WEB}/w/${ws.id}/knowledge`, { waitUntil: "networkidle" }); const cards = () => page.locator("li"); const first = await cards().count(); check("首屏有列表", first > 0, `count=${first}`); let rounds = 0; while (rounds < 20) { const more = page.getByRole("button", { name: "加载更多" }); if ((await more.count()) === 0) break; const before = await cards().count(); await more.first().click(); const grew = async () => (await cards().count()) > before; for (let i = 0; i < 40 && !(await grew()); i += 1) await page.waitForTimeout(100); if (!(await grew())) break; rounds += 1; } const shown = await cards().count(); check("点「加载更多」能把列表翻到底", shown >= total, `shown=${shown} total=${total}`); check("翻页接口没有报错", bad.length === 0, bad.slice(0, 3).join(" | ")); const toast = await page.locator("text=服务器内部错误").count(); check("页面上没有「服务器内部错误」", toast === 0); check("控制台零报错", errors.length === 0, errors.slice(0, 2).join(" | ")); await page.screenshot({ path: "e2e/artifacts/knowledge-paging.png" }); await browser.close(); console.log(`\n${fail === 0 ? "ALL PASS" : "SOME FAILED"} — pass ${pass}, fail ${fail}`); process.exit(fail === 0 ? 0 : 1); } main().catch((err) => { console.error("脚本异常 —", err?.message ?? err); process.exit(1); });