Files
DadaoTask/web/e2e/knowledge-read.mjs
T
Steven 35fdea7260
CI / backend (push) Canceled after 0s
test(web): e2e 与投图脚本改成密码登录,补生产的阅读页实拍
生产短信登录已经关了,脚本里那条发码路径跑不通;统一走 PASSWORD,读不到才回落到短信。
2026-11-03 12:15:31 +08:00

160 lines
5.8 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}` : ""}`);
}
}
// 生产关了短信登录: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(
"正文列限宽到可读宽度(≤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);