diff --git a/web/e2e/knowledge-paging.mjs b/web/e2e/knowledge-paging.mjs new file mode 100644 index 0000000..59d0e3f --- /dev/null +++ b/web/e2e/knowledge-paging.mjs @@ -0,0 +1,124 @@ +/** + * 知识列表翻页的界面验收:点「加载更多」能把 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 { 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 = process.env.CODE ?? "123456"; +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}`); + } +} + +async function login() { + const r = await fetch(`${API}/auth/sms/send`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone: PHONE }), + }); + const sent = await r.json(); + if (sent.code !== 0) throw new Error(`发码失败:${sent.message}`); + 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 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() { + void CODE; + 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); +});