327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
/** UI contract checks use isolated API fixtures; real storage/auth remain covered by guidance-integration.ts. */
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { chromium } from "playwright-core";
|
|
import { documentInput, parseDocumentXml } from "../src/lib/guidance-input";
|
|
import { guidanceXml, type Guidance } from "../src/content/guides/catalog";
|
|
const base = process.env.WORKSPACE_TEST_URL;
|
|
const owner = "11111111-1111-4111-8111-111111111111";
|
|
const authored: Guidance = {
|
|
id: "user-" + owner + "-review",
|
|
revision: 1,
|
|
kind: "guide",
|
|
title: "准备一次清晰的代码审查",
|
|
summary: "用明确的问题和证据,完成一次有用的代码审查。",
|
|
applies: "提交合并请求前",
|
|
body: "先理解改动目标,再核对实现与验证。",
|
|
steps: [],
|
|
checks: [],
|
|
links: [],
|
|
tags: [],
|
|
};
|
|
test(
|
|
"workspace filters, reading tabs, account states and editor work on desktop and mobile",
|
|
{ skip: !base },
|
|
async () => {
|
|
const browser = await chromium.launch({
|
|
executablePath:
|
|
process.env.CHROMIUM_PATH ||
|
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
headless: true,
|
|
args: ["--no-sandbox"],
|
|
});
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 1000 },
|
|
});
|
|
let signedIn = false,
|
|
previewError = false,
|
|
requestedAuthor: string | null = null,
|
|
published: Guidance | null = null;
|
|
const errors: string[] = [];
|
|
try {
|
|
await context.route("**/api/account/session", (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
user: signedIn
|
|
? {
|
|
id: owner,
|
|
email: "editor@example.com",
|
|
verified_at: "2026-10-29T00:00:00Z",
|
|
}
|
|
: null,
|
|
},
|
|
}),
|
|
);
|
|
await context.route(
|
|
(url) => url.pathname === "/api/documents",
|
|
async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
requestedAuthor = new URL(route.request().url()).searchParams.get(
|
|
"author",
|
|
);
|
|
return route.fulfill({
|
|
json: { documents: [{ ...authored, author: owner }] },
|
|
});
|
|
}
|
|
published = documentInput.parse(route.request().postDataJSON());
|
|
return route.fulfill({ status: 201, json: { document: published } });
|
|
},
|
|
);
|
|
await context.route("**/api/documents/preview", (route) => {
|
|
if (previewError)
|
|
return route.fulfill({
|
|
status: 400,
|
|
json: { error: "输入不符合协议", fields: ["applies"] },
|
|
});
|
|
let document: Guidance;
|
|
try {
|
|
document =
|
|
route.request().headers()["content-type"] === "application/xml"
|
|
? parseDocumentXml(route.request().postData() || "")
|
|
: documentInput.parse(route.request().postDataJSON());
|
|
} catch {
|
|
return route.fulfill({
|
|
status: 400,
|
|
json: { error: "输入不符合协议" },
|
|
});
|
|
}
|
|
return route.fulfill({
|
|
json: {
|
|
document,
|
|
xml: guidanceXml(document),
|
|
links: document.links.map((link) => ({ ...link, resolved: true })),
|
|
},
|
|
});
|
|
});
|
|
await context.route("**/api/documents/" + authored.id, (route) =>
|
|
route.fulfill({ json: { document: authored } }),
|
|
);
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(10000);
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
await page.goto(base + "/documents", { waitUntil: "networkidle" });
|
|
await page
|
|
.getByRole("heading", { name: authored.title, exact: true })
|
|
.waitFor();
|
|
assert.equal(await page.title(), "社区文档 · 织页");
|
|
const search = page.getByRole("textbox", { name: "搜索文档" });
|
|
await search.fill("不存在的文档");
|
|
await search.press("Enter");
|
|
await page
|
|
.getByRole("heading", { name: "没有匹配文档", exact: true })
|
|
.waitFor();
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
assert.equal(await search.inputValue(), "不存在的文档");
|
|
await page
|
|
.getByRole("button", { name: "清除所有筛选", exact: true })
|
|
.click();
|
|
await page
|
|
.getByRole("heading", { name: authored.title, exact: true })
|
|
.waitFor();
|
|
await page.getByRole("tab", { name: /事实依据/ }).focus();
|
|
await page.keyboard.press("Home");
|
|
await page.getByRole("tab", { name: /全部文档/ }).waitFor();
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR) {
|
|
await mkdir(process.env.WORKSPACE_SCREENSHOT_DIR, { recursive: true });
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/directory.png",
|
|
fullPage: true,
|
|
});
|
|
}
|
|
await page.goto(base + "/guide/review-changes?revision=1", {
|
|
waitUntil: "networkidle",
|
|
});
|
|
await page.getByRole("tab", { name: "XML 源文档" }).click();
|
|
assert.match(await page.locator("pre").innerText(), /linked-guidance/);
|
|
await page.getByRole("tab", { name: /版本记录/ }).click();
|
|
await page.getByText("当前阅读", { exact: true }).waitFor();
|
|
await page.getByRole("tab", { name: "文档概览" }).click();
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR)
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/reader.png",
|
|
fullPage: true,
|
|
});
|
|
await page.goto(base + "/account", { waitUntil: "networkidle" });
|
|
await page.getByRole("button", { name: "创建账户", exact: true }).click();
|
|
await page.getByRole("heading", { name: "创建你的账户" }).waitFor();
|
|
await page
|
|
.getByLabel("密码", { exact: true })
|
|
.fill("a-long-example-password");
|
|
await page.getByRole("button", { name: "显示密码" }).click();
|
|
assert.equal(
|
|
await page.getByLabel("密码", { exact: true }).getAttribute("type"),
|
|
"text",
|
|
);
|
|
await page.getByRole("button", { name: "隐藏密码" }).click();
|
|
await page.getByLabel("密码", { exact: true }).fill("");
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR)
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/account.png",
|
|
fullPage: true,
|
|
});
|
|
await page.goto(base + "/write", { waitUntil: "networkidle" });
|
|
await page.getByRole("heading", { name: "登录后开始编写" }).waitFor();
|
|
signedIn = true;
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
await page.getByLabel("文档短名", { exact: true }).fill("workspace-test");
|
|
await page.getByLabel("标题", { exact: false }).fill("界面回归验证文档");
|
|
await page
|
|
.getByLabel("一句话摘要", { exact: false })
|
|
.fill("可独立使用的指导");
|
|
await page
|
|
.getByLabel("适用条件", { exact: false })
|
|
.fill("检查界面流程时");
|
|
await page
|
|
.getByLabel("正文", { exact: false })
|
|
.fill("先判断适用条件,再阅读正文。");
|
|
await page
|
|
.getByLabel("建议步骤", { exact: true })
|
|
.fill("第一步\n\n第二步");
|
|
previewError = true;
|
|
await page
|
|
.getByRole("button", { name: "校验并预览", exact: true })
|
|
.click();
|
|
await page.getByRole("alert").filter({ hasText: "适用条件" }).waitFor();
|
|
previewError = false;
|
|
await page
|
|
.getByRole("button", { name: "校验并预览", exact: true })
|
|
.click();
|
|
await page.getByRole("dialog").waitFor({ state: "visible" });
|
|
await page.getByRole("button", { name: "返回编辑" }).click();
|
|
assert.equal(
|
|
await page.getByLabel("正文", { exact: false }).inputValue(),
|
|
"先判断适用条件,再阅读正文。",
|
|
);
|
|
await page.getByRole("button", { name: "添加条件链接" }).click();
|
|
await page.getByRole("button", { name: "移除关联文档 1" }).click();
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR) {
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/editor.png",
|
|
fullPage: true,
|
|
});
|
|
}
|
|
await page.getByRole("button", { name: "发布文档", exact: true }).click();
|
|
await page
|
|
.getByRole("status")
|
|
.filter({ hasText: "已公开发布" })
|
|
.waitFor();
|
|
assert.ok(published);
|
|
assert.deepEqual((published as Guidance).steps, ["第一步", "第二步"]);
|
|
assert.equal(
|
|
(published as Guidance).id,
|
|
"user-" + owner + "-workspace-test",
|
|
);
|
|
await page.goto(base + "/documents?scope=mine", {
|
|
waitUntil: "networkidle",
|
|
});
|
|
assert.equal(requestedAuthor, owner);
|
|
await page
|
|
.getByRole("heading", { name: authored.title, exact: true })
|
|
.waitFor();
|
|
await page.goto(base + "/write?id=" + authored.id, {
|
|
waitUntil: "networkidle",
|
|
});
|
|
assert.equal(
|
|
await page.getByLabel("文档短名", { exact: true }).isDisabled(),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await page.getByLabel("标题", { exact: false }).inputValue(),
|
|
authored.title,
|
|
);
|
|
await page.getByRole("button", { name: "协议 XML", exact: true }).click();
|
|
const xmlInput = page.getByLabel("XML 输入");
|
|
assert.match(await xmlInput.inputValue(), /revision="2"/);
|
|
await xmlInput.fill(
|
|
(await xmlInput.inputValue()).replace(
|
|
authored.title,
|
|
"通过 XML 更新的文档",
|
|
),
|
|
);
|
|
await page
|
|
.getByRole("button", { name: "可视化编辑", exact: true })
|
|
.click();
|
|
assert.equal(
|
|
await page.getByLabel("标题", { exact: false }).inputValue(),
|
|
"通过 XML 更新的文档",
|
|
);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto(base + "/documents", { waitUntil: "networkidle" });
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
),
|
|
true,
|
|
);
|
|
await page.getByRole("button", { name: "打开导航" }).click();
|
|
await page
|
|
.getByRole("dialog", { name: "工作区导航" })
|
|
.waitFor({ state: "visible" });
|
|
await page.keyboard.press("Escape");
|
|
assert.equal(
|
|
await page.getByRole("dialog", { name: "工作区导航" }).isVisible(),
|
|
false,
|
|
);
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR)
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/mobile-directory.png",
|
|
fullPage: true,
|
|
});
|
|
await page.goto(base + "/guide/review-changes?revision=1", {
|
|
waitUntil: "networkidle",
|
|
});
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
),
|
|
true,
|
|
);
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR)
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/mobile-reader.png",
|
|
fullPage: true,
|
|
});
|
|
await page.goto(base + "/account", { waitUntil: "networkidle" });
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
),
|
|
true,
|
|
);
|
|
await page.goto(base + "/write", { waitUntil: "networkidle" });
|
|
await page.getByLabel("文档短名", { exact: true }).waitFor();
|
|
await page.getByRole("button", { name: "协议 XML", exact: true }).click();
|
|
await page.getByLabel("XML 输入").waitFor();
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
),
|
|
true,
|
|
);
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR)
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/mobile-editor.png",
|
|
fullPage: true,
|
|
});
|
|
await page.goto(base + "/protocol", { waitUntil: "networkidle" });
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= window.innerWidth,
|
|
),
|
|
true,
|
|
);
|
|
if (process.env.WORKSPACE_SCREENSHOT_DIR)
|
|
await page.screenshot({
|
|
path: process.env.WORKSPACE_SCREENSHOT_DIR + "/mobile-protocol.png",
|
|
fullPage: true,
|
|
});
|
|
assert.deepEqual(errors, []);
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
}
|
|
},
|
|
);
|