/** UI fixtures run in a fresh context; real storage and authentication have separate coverage. */ import assert from "node:assert/strict"; import { visitBrowserDocument } from "./brand-navigation.mjs"; import { mkdir } from "node:fs/promises"; import type { Browser } from "playwright-core"; import { documentInput, parseDocumentXml } from "../src/lib/guidance-input"; import { guidanceXml, type Guidance } from "../src/content/guides/catalog"; 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: [], }; export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser) { 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[] = []; let sessionReplies = 0; try { await context.route("**/api/account/session", async (route) => { await route.fulfill({ status: 200, json: { user: signedIn ? { id: owner, email: "editor@example.com", verified_at: "2026-10-29T00:00:00Z", } : null, }, }); sessionReplies += 1; }); 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(60000); page.setDefaultNavigationTimeout(60000); page.on("pageerror", (error) => errors.push(error.message)); page.on("crash", () => errors.push("Browser renderer crashed")); const pending = new Set(), network: string[] = []; page.on("request", request => pending.add(request.url())); page.on("requestfinished", request => pending.delete(request.url())); page.on("requestfailed", request => {pending.delete(request.url());network.push(new URL(request.url()).pathname+": "+request.failure()?.errorText);}); async function navigate(url: string, reload = false) { const previousSessionReplies = sessionReplies; try { const navigation = await visitBrowserDocument(page, url, reload); assert.ok(navigation?.ok(), "Navigation returns a successful document"); if (new URL(url).pathname === "/account") { await page.getByRole("heading", { name: signedIn ? "你的账户" : "欢迎回来", exact: true }).waitFor(); } else { await page.getByText(signedIn ? "editor@example.com" : "登录你的账户",{exact:true}).first().waitFor({state:"attached"}); } await page.getByText("正在检查账户状态…", {exact: true}).waitFor({state: "hidden"}); assert.ok(sessionReplies > previousSessionReplies, "The new document requests its account fixture and renders the resulting account state"); } catch (error) { const probeStarted=Date.now(); const server=await fetch(url,{signal:AbortSignal.timeout(3000)}).then(async response=>{await response.body?.cancel();return {status:response.status,elapsedMs:Date.now()-probeStarted}}).catch(error=>({error:error instanceof Error?error.name:"unknown",elapsedMs:Date.now()-probeStarted})); console.error("Workspace UI navigation diagnostics",JSON.stringify({url:page.url(),browserConnected:browser.isConnected(),server,pending:[...pending].map(value=>new URL(value).pathname),network:network.slice(-20),errors,body:(await page.locator("body").innerText().catch(()=>"unavailable")).slice(0,1600)})); throw error; } } await navigate(base + "/documents"); 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 navigate(page.url(), true); await page.getByRole("heading",{name:"没有匹配文档",exact:true}).waitFor(); await page.waitForFunction(() => document.querySelector('input[aria-label="搜索文档"]')?.value === "不存在的文档"); 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 navigate(base + "/guide/review-changes?revision=1"); 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 navigate(base + "/account"); assert.equal(await page.getByRole("complementary").count(), 0, "Account page has no workspace sidebar"); assert.equal(await page.getByRole("link", { name: "创建文档", exact: true }).count(), 0); await page.getByRole("link", { name: "返回首页", exact: true }).waitFor(); const accountFits = async () => assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1 && document.documentElement.scrollHeight <= innerHeight + 1), "Account page fits the viewport on both axes"); await accountFits(); await context.route("**/api/account/login", route => route.fulfill({status: 401, json: {error: "测试登录失败,请重试"}})); await page.getByLabel("邮箱地址", {exact: true}).fill("fixture@example.com"); await page.getByLabel("密码", {exact: true}).fill("fixture-password-long"); await page.waitForFunction(() => document.getAnimations().every(animation => animation.playState !== "running")); const beforeMessage = await page.getByLabel("邮箱地址", {exact: true}).boundingBox(); await page.locator("form").getByRole("button", {name: "登录", exact: true}).click(); await page.getByRole("alert").getByText("测试登录失败,请重试").waitFor(); const afterMessage = await page.getByLabel("邮箱地址", {exact: true}).boundingBox(); assert.equal(afterMessage?.y, beforeMessage?.y, "Status messages must not move the form"); await accountFits(); await page.getByRole("button", {name: "关闭提示", exact: true}).click(); await context.route("**/api/account/forgot", route => route.fulfill({json: {ok: true, message: "若账户符合条件,重设链接将发送到邮箱。"}})); await page.getByRole("button", {name: "忘记密码?", exact: true}).click(); await page.getByRole("heading", {name: "找回你的账户", exact: true}).waitFor(); await page.getByLabel("邮箱地址", {exact: true}).fill("fixture@example.com"); await page.getByRole("button", {name: "发送重设链接", exact: true}).click(); await page.getByRole("status").getByText("若账户符合条件,重设链接将发送到邮箱。").waitFor(); await page.getByRole("button", {name: "返回登录", exact: true}).click(); 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, }); for (const viewport of [{width: 390, height: 844}, {width: 320, height: 568}]) { await page.setViewportSize(viewport); await accountFits(); await page.getByLabel("邮箱地址", {exact: true}).waitFor({state: "visible"}); } await page.setViewportSize({width: 1440, height: 1000}); await context.route("**/api/account/reset", async route => { const body = route.request().postDataJSON(); assert.equal(body.token, "f".repeat(64)); assert.equal(body.password, "replacement-fixture-password"); await route.fulfill({json: {ok: true}}); }); await visitBrowserDocument(page, base + "/account?reset=" + "f".repeat(64)); await page.getByRole("heading", {name: "设置新密码", exact: true}).waitFor(); assert.equal(new URL(page.url()).searchParams.has("reset"), false, "Recovery token is removed from the visible URL"); await page.getByLabel("新密码", {exact: true}).fill("replacement-fixture-password"); await page.getByLabel("确认新密码", {exact: true}).fill("replacement-fixture-password"); await page.getByRole("button", {name: "保存新密码", exact: true}).click(); await page.getByRole("heading", {name: "欢迎回来", exact: true}).waitFor(); await accountFits(); await navigate(base + "/write"); await page.getByRole("heading", { name: "登录后开始编写" }).waitFor(); signedIn = true; await navigate(page.url(), true); 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 navigate(base + "/documents?scope=mine"); await page .getByRole("heading", { name: authored.title, exact: true }) .waitFor(); assert.equal(requestedAuthor, owner); await navigate(base + "/write?id=" + authored.id); await page.waitForFunction(title=>Array.from(document.querySelectorAll("input")).some(input=>input.value===title),authored.title); 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 navigate(base + "/documents"); 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 navigate(base + "/guide/review-changes?revision=1"); 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 navigate(base + "/account"); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), true, ); await navigate(base + "/write"); 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 navigate(base + "/protocol"); 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(); } }