Files
DadaoTask/web/e2e/knowledge-read.mjs
T
Steven d34a6ac69e
CI / backend (push) Canceled after 0s
feat(knowledge): 阅读改成独立整页,抽屉只用来编辑
知识原来一律在抽屉里打开,正文贴着屏幕右半边,长文读十几屏眼睛一直在右边。
阅读是「坐下来读一会儿」,编辑是「改两句就走」,两件事不该共用一个容器。

- 新增阅读页 /w/:wsId/knowledge/:itemId:正文列限到 40rem 居中,28px 标题,
  更新/发布/切片行,标签与来源,动作行放编辑与发布/归档/恢复
- 列表页与搜索结果改成跳阅读页,不再弹抽屉
- KnowledgeDrawer 加 initialEditing:进来就是编辑器,保存或取消直接关
- 文案常量搬到 lib/knowledge.ts,列表页与阅读页共用一份

顺带修三个 e2e 脚本:新建后进阅读页没有「关闭」按钮可点、侧栏色点按钮让树
顺序断言多出空串、翻页脚本还在用已关闭的短信登录(改密码登录)。
2026-11-03 12:12:51 +08:00

158 lines
5.7 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 存在),不是编辑器
* ③ 正文列的宽度 ≤ 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);