知识原来一律在抽屉里打开,正文贴着屏幕右半边,长文读十几屏眼睛一直在右边。 阅读是「坐下来读一会儿」,编辑是「改两句就走」,两件事不该共用一个容器。 - 新增阅读页 /w/:wsId/knowledge/:itemId:正文列限到 40rem 居中,28px 标题, 更新/发布/切片行,标签与来源,动作行放编辑与发布/归档/恢复 - 列表页与搜索结果改成跳阅读页,不再弹抽屉 - KnowledgeDrawer 加 initialEditing:进来就是编辑器,保存或取消直接关 - 文案常量搬到 lib/knowledge.ts,列表页与阅读页共用一份 顺带修三个 e2e 脚本:新建后进阅读页没有「关闭」按钮可点、侧栏色点按钮让树 顺序断言多出空串、翻页脚本还在用已关闭的短信登录(改密码登录)。
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 知识阅读页的验收:点开一条知识应该离开列表、进整页阅读,正文落在视线中间。
|
||||
*
|
||||
* 断言的是用户提的那件事,而不是"有个 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);
|
||||
Reference in New Issue
Block a user