Files
DadaoTask/web/e2e/knowledge-read.mjs
T
Steven da31083df2
CI / backend (push) Canceled after 0s
fix(knowledge): 阅读页正文列放宽,宽屏上不再留白比字多
1440 的屏幕上 40rem 正文只有 640px,两边各空出半屏,用户直接发截图说"留白比内容都多"。
正文列 56rem(约 55 个汉字一行)、卡片 78rem,行宽到头但不用来回扫行。
e2e 的宽度断言跟着改成 720–920px,避免以后再窄回去没人拦。
2026-11-03 12:23:53 +08:00

160 lines
5.9 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.
/**
* 知识阅读页的验收:点开一条知识应该离开列表、进整页阅读,正文落在视线中间。
*
* 断言的是用户提的那件事,而不是"有个 drawer":
* ① 点卡片后 URL 变成 /w/<ws>/knowledge/<id>,列表页的元素不在了
* ② 正文渲染成样式(h2 存在),不是编辑器
* ③ 正文列限宽但别太窄(720–920px,约 50 来个汉字一行),且在内容区里居中
* ④ 点「编辑」才出现编辑器(抽屉),保存/取消都收回去
*/
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}` : ""}`);
}
}
// 生产关了短信登录:PASSWORD 优先,其次读本机那份 dev 密码
const password = (() => {
if (process.env.PASSWORD) return process.env.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(
"正文列宽度适合长文(720–920px,宽屏不留大片空白)",
width >= 720 && width <= 920,
`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);