/** * 检索 e2e:发布的知识能被搜到,草稿搜不到,结果能点回原文。 * * 只验证"用户能不能用上",切片与打分逻辑由后端测试覆盖。 */ import { chromium } from "playwright-core"; const WEB = process.env.WEB ?? "http://127.0.0.1:5180"; const API = process.env.API ?? "http://127.0.0.1:8100/api/v1"; const stamp = String(Date.now()).slice(-7); const phone = `134${stamp}0`; let failures = 0; function check(name, ok, extra = "") { console.log(`${ok ? "PASS" : "FAIL"} ${name}${extra ? ` — ${extra}` : ""}`); if (!ok) failures += 1; } async function call(method, path, body, token) { const res = await fetch(`${API}${path}`, { method, headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}), ...(method === "GET" ? {} : { "idempotency-key": `e2e-${Math.random()}` }), }, body: body ? JSON.stringify(body) : undefined, }); const text = await res.text(); const json = text ? JSON.parse(text) : {}; if (json.code !== 0) throw new Error(`${method} ${path} -> ${json.code} ${json.message}`); return json.data; } const browser = await chromium.launch({ executablePath: process.env.CHROME ?? `${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`, }); const consoleErrors = []; const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const page = await context.newPage(); page.on("console", (m) => { if (m.type() === "error") consoleErrors.push(m.text()); }); page.on("response", (r) => { if (r.status() >= 400) consoleErrors.push(`${r.status()} ${r.url()}`); }); try { await call("POST", "/auth/sms/send", { phone }); const login = await call("POST", "/auth/login", { phone, code: "123456" }); const token = login.tokens.access_token; const ws = await call("POST", "/workspaces", { name: `检索验收 ${stamp}` }, token); const published = await call( "POST", `/workspaces/${ws.id}/knowledge`, { title: "快递单号对不上怎么处理", doc_type: "howto", body_md: "## 步骤\n\n先在顺丰后台按手机号反查单号,再和发货记录逐一核对。\n\n如果差超过两天,直接找客服开异常件工单。", }, token, ); await call( "POST", `/workspaces/${ws.id}/knowledge/${published.id}/status`, { action: "publish", version: published.version }, token, ); const draft = await call( "POST", `/workspaces/${ws.id}/knowledge`, { title: "还没核对的草稿", doc_type: "note", body_md: "这条写的是异常件的赔付口径,尚未定稿。" }, token, ); await page.addInitScript( ([access, refresh]) => { localStorage.setItem("dadao.access", access); localStorage.setItem("dadao.refresh", refresh); }, [token, login.tokens.refresh_token], ); await page.goto(`${WEB}/w/${ws.id}/knowledge`, { waitUntil: "networkidle" }); const box = page.getByPlaceholder(/搜标题或正文/); await box.fill("异常件单号核对"); await box.press("Enter"); await page.getByText(/命中 \d+ 段/).waitFor({ timeout: 10_000 }); const summary = await page.getByText(/命中 \d+ 段/).first().textContent(); check("检索返回了结果", /命中 [1-9]\d* 段/.test(summary ?? ""), summary ?? ""); check("命中里带标题", await page.getByText("快递单号对不上怎么处理", { exact: false }).first().isVisible()); check("草稿不会出现在检索结果里", !(await page.getByText("还没核对的草稿", { exact: false }).count())); await page.screenshot({ path: "e2e/artifacts/retrieval-hits.png", fullPage: false }); await page.getByRole("button", { name: "打开原文", exact: true }).first().click(); await page.getByText(/已发布 · 版本 \d+/, { exact: false }).waitFor({ timeout: 10_000 }); check("命中能点回原文", true); await page.getByRole("button", { name: "关闭", exact: true }).click(); await box.fill("量子纠缠"); await box.press("Enter"); await page.getByText("没有检索到相关内容", { exact: false }).waitFor({ timeout: 10_000 }); check("不相干的关键词宁可返回空", true); check("浏览器控制台与网络无报错", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | ")); } catch (err) { failures += 1; console.log(`FAIL 脚本异常 — ${err.message}`); await page.screenshot({ path: "e2e/artifacts/retrieval-failure.png" }).catch(() => {}); console.log((await page.textContent("body"))?.slice(0, 600)); } finally { await browser.close(); console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1); }