/** * 知识阅读页的验收:点开一条知识应该离开列表、进整页阅读,正文落在视线中间。 * * 断言的是用户提的那件事,而不是"有个 drawer": * ① 点卡片后 URL 变成 /w//knowledge/,列表页的元素不在了 * ② 正文渲染成样式(h2 存在),不是编辑器 * ③ 正文列的宽度 ≤ 46rem 且在内容区里居中(左右留白差不多) * ④ 点「编辑」才出现编辑器(抽屉),保存/取消都收回去 */ import { chromium } from "playwright-core"; import { mkdirSync, readFileSync } from "node:fs"; 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 PHONE = process.env.PHONE ?? "13900000001"; const OUT = "e2e/artifacts"; const PWD_FILE = new URL("../../.local/e2e-account-password.txt", import.meta.url); 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 ? ` — ${extra}` : ""}`); } } const password = (() => { try { return readFileSync(PWD_FILE, "utf8").trim(); } catch { return ""; } })(); const browser = await chromium.launch({ executablePath: process.env.CHROME ?? `${process.env.LOCALAPPDATA}/ms-playwright/chromium-1223/chrome-win64/chrome.exe`, }); const page = await browser.newPage({ viewport: { width: 1440, height: 960 } }); const errors = []; page.on("console", (m) => m.type() === "error" && errors.push(m.text())); page.on("pageerror", (e) => errors.push(String(e))); mkdirSync(OUT, { recursive: true }); try { const r = await fetch(`${API}/auth/login/password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: PHONE, password }), }); const login = await r.json(); if (login.code !== 0) throw new Error(`登录失败:${JSON.stringify(login)}`); const ws = login.data.user ? null : null; void ws; const token = login.data.tokens.access_token; const refresh = login.data.tokens.refresh_token; // 找一个有正文的知识 const listResp = await fetch(`${API}/workspaces`, { headers: { Authorization: `Bearer ${token}` }, }).then((x) => x.json()); const workspaces = listResp.data.items ?? listResp.data.workspaces ?? listResp.data; const workspace = workspaces.find((w) => w.name?.includes("备忘")) ?? workspaces[0]; const wsId = workspace.id; const docsResp = await fetch(`${API}/workspaces/${wsId}/knowledge?limit=50`, { headers: { Authorization: `Bearer ${token}` }, }).then((x) => x.json()); const docs = docsResp.data.items.filter((d) => (d.body_md ?? "").length > 200); if (docs.length === 0) throw new Error("没有正文够长的知识可用来验收"); const target = docs[0]; await page.addInitScript( ([access, refreshToken]) => { localStorage.setItem("dadao.access", access); localStorage.setItem("dadao.refresh", refreshToken); }, [token, refresh], ); await page.goto(`${WEB}/w/${wsId}/knowledge`); await page.waitForSelector("li button"); await page .locator("li") .filter({ hasText: target.title.slice(0, 8) }) .first() .getByRole("button") .first() .click(); const onDoc = await page .waitForURL(new RegExp(`/w/${wsId}/knowledge/${target.id}$`), { timeout: 10000 }) .then(() => true) .catch(() => false); check("点卡片进的是独立阅读页(不是抽屉)", onDoc, page.url()); await page.waitForSelector("h1"); const title = await page.locator("h1").first().innerText(); check("整页标题就是这条知识的标题", title.trim() === target.title.trim(), title); // 正文渲染成样式:markdown 里的标题/列表应该变成真元素 const hasProse = (await page.locator(".prose-dadao").count()) > 0; const hasEditor = (await page.locator(".vditor-ir").count()) > 0; check("阅读时是渲染后的正文,不是编辑器", hasProse && !hasEditor, `prose=${hasProse} editor=${hasEditor}`); // 正文列在内容区里居中、宽度适合阅读 const box = await page.locator(".prose-dadao").first().boundingBox(); const main = await page.locator("main").first().boundingBox(); if (box && main) { const leftGap = box.x - main.x; const rightGap = main.x + main.width - (box.x + box.width); const width = box.width; check( "正文列限宽到可读宽度(≤46rem≈736px)", width <= 760, `width=${Math.round(width)}`, ); check( "正文列在内容区里居中", Math.abs(leftGap - rightGap) < 40, `left=${Math.round(leftGap)} right=${Math.round(rightGap)}`, ); } else { check("正文列在内容区里居中", false, "拿不到布局尺寸"); } await page.screenshot({ path: `${OUT}/knowledge-read-page.png` }); // 编辑才是抽屉 await page.getByRole("button", { name: "编辑", exact: true }).click(); await page.waitForSelector(".vditor-ir", { timeout: 10000 }); check("点「编辑」才出现编辑器(抽屉)", (await page.locator(".vditor-ir").count()) > 0); await page.screenshot({ path: `${OUT}/knowledge-edit-drawer.png` }); await page.getByRole("button", { name: "取消", exact: true }).click(); const closed = await page .waitForSelector(".vditor-ir", { state: "detached", timeout: 10000 }) .then(() => true) .catch(() => false); check("取消编辑后回到阅读页", closed); check("控制台无报错", errors.length === 0, errors.slice(0, 2).join(" | ")); } catch (err) { fail += 1; console.log(`FAIL 脚本异常 — ${err}`); } finally { await browser.close(); } console.log(`\n${fail === 0 ? "ALL PASS" : "FAILED"} — pass ${pass}, fail ${fail}`); process.exit(fail === 0 ? 0 : 1);