/** 页面/会话/草稿归属:真实页面与真实登录,只有外部 API 响应受控。 */ import assert from "node:assert/strict"; import { test } from "node:test"; import { appOrigin, backendOrigin, login, loginAccount, loginPassword, scenario, } from "./helpers/console-browser.mjs"; function deferred() { let resolve; const promise = new Promise((done) => (resolve = done)); return { promise, resolve }; } function respond(route, data, status = 200) { return route.fulfill({ json: { status, msg: status === 200 ? "" : "本地延迟失败", data }, headers: { "Access-Control-Allow-Origin": "*" }, }); } async function boundary(page, handle) { const requests = []; await page .context() .route(`${backendOrigin}/console/v1/**`, async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname.replace(/^\/console\/v1/, ""); const entry = { method: request.method(), path, authenticated: !!request.headers().authorization, }; requests.push(entry); if (await handle(route, path, entry, url)) return; return route.fallback(); }); return requests; } async function releaseResponse(page, release, path) { const response = page.waitForResponse( (reply) => new URL(reply.url()).pathname === `/console/v1${path}`, ); release.resolve(); await (await response).finished(); // Absence assertions observe a bounded idle window after the real fetch has completed. await page.waitForLoadState("networkidle"); } async function signInOnCurrentDocument(page) { await page.getByLabel("账号", { exact: true }).fill(loginAccount); await page.getByLabel("密码", { exact: true }).fill(loginPassword); await page.getByRole("button", { name: "登录", exact: true }).click(); await page.waitForURL((url) => url.pathname !== "/login"); await page.getByRole("button", { name: "用户菜单", exact: true }).waitFor(); } async function logoutAndSignIn(page) { await page.getByRole("button", { name: "用户菜单", exact: true }).click(); await page.getByRole("menuitem", { name: "退出登录", exact: true }).click(); await page.waitForURL((url) => url.pathname === "/login"); await signInOnCurrentDocument(page); } for (const transition of ["current", "leave", "reauthenticate"]) { test( `message detail ownership after ${transition}`, { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); const requests = await boundary(page, async (route, path) => { if (path === "/account/notification-preferences") { await respond(route, { email_enabled: false, sms_enabled: false, email_available: true, sms_available: false, }); return true; } if (path === "/client_care/mail/list") { await respond(route, { list: [], count: 0 }); return true; } if (path === "/client_care/mail/701") { started.resolve(); await release.promise; await respond(route, { mail: { id: 701, title: "归属测试消息", content: "
本地正文
", }, }); return true; } if (path === "/client_care/mail/read") { await respond(route, {}); return true; } return false; }); await login(page, "/messages?id=701"); await started.promise; if (transition !== "current") await page.keyboard.press("Escape"); if (transition === "leave") { await page .getByRole("navigation", { name: "会员中心导航" }) .getByRole("link", { name: "SSH 密钥", exact: true }) .click(); await page.waitForURL((url) => url.pathname === "/security/ssh-keys"); } if (transition === "reauthenticate") await logoutAndSignIn(page); await releaseResponse(page, release, "/client_care/mail/701"); const reads = requests.filter( (entry) => entry.path === "/client_care/mail/read", ); assert.equal( reads.length, transition === "current" ? 1 : 0, "Only the current detail owner may derive mark-read HTTP", ); return { transition, markReadRequests: reads.length, authenticated: reads.every((entry) => entry.authenticated), }; }), ); } for (const routePath of ["/tickets/create", "/tickets/71"]) { for (const transition of ["current", "leave"]) { test( `ticket attachment queue ${routePath} after ${transition}`, { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); let uploads = 0; const requests = await boundary(page, async (route, path) => { if (path === "/ticket/config") { await respond(route, { ticket_notice_open: 0 }); return true; } if (["/ticket/type", "/ticket/department"].includes(path)) { await respond(route, { list: [{ id: 1, name: "本地支持" }] }); return true; } if (path === "/ticket/71") { await respond(route, { ticket: { id: 71, title: "本地工单", content: "本地内容
", can_operate: 1, attachment: [], replies: [], create_time: 1700000000, }, }); return true; } if (path === "/upload") { uploads += 1; if (uploads === 1) { started.resolve(); await release.promise; } await respond(route, { save_name: `local-attachment-${uploads}.txt`, }); return true; } return false; }); await login(page, routePath); const input = page.locator('input[type="file"]'); await input.setInputFiles( [1, 2].map((id) => ({ name: `local-${id}.txt`, mimeType: "text/plain", buffer: Buffer.from("non-sensitive local fixture"), })), ); await started.promise; if (transition === "leave") { await page .getByRole("navigation", { name: "会员中心导航" }) .getByRole("link", { name: "SSH 密钥", exact: true }) .click(); await page.waitForURL( (url) => url.pathname === "/security/ssh-keys", ); } await releaseResponse(page, release, "/upload"); assert.equal( uploads, transition === "current" ? 2 : 1, "A departed upload owner must not send the next attachment", ); if (transition === "current") await page.getByText("local-2.txt", { exact: true }).waitFor(); return { routePath, transition, uploads, authenticated: requests .filter((entry) => entry.path === "/upload") .every((entry) => entry.authenticated), }; }), ); } } for (const transition of ["current", "leave"]) { test( `password reset navigation after ${transition}`, { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); await boundary(page, async (route, path) => { if (path !== "/account/password_reset") return false; started.resolve(); await release.promise; await respond(route, {}); return true; }); await page.goto(`${appOrigin}/forgot-password`); await page.getByLabel("手机号", { exact: true }).fill("13800138000"); await page.getByLabel("验证码 *", { exact: true }).fill("123456"); await page .getByLabel("新密码 *", { exact: true }) .fill("local-test-password"); await page .getByLabel("确认新密码 *", { exact: true }) .fill("local-test-password"); await page .getByRole("button", { name: "重置密码", exact: true }) .click(); await started.promise; if (transition === "leave") { await page .getByRole("link", { name: "想起密码了?返回登录", exact: true }) .click(); await page .getByRole("link", { name: "注册账号", exact: true }) .click(); await page.waitForURL((url) => url.pathname === "/register"); } await releaseResponse(page, release, "/account/password_reset"); assert.equal( new URL(page.url()).pathname, transition === "current" ? "/login" : "/register", "Only the current reset operation may navigate", ); return { transition, finalRoute: new URL(page.url()).pathname }; }), ); } for (const action of ["options", "submit", "reply", "urge", "close"]) { test( `ticket ${action} cannot derive work after leaving`, { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); const detail = ["reply", "urge", "close"].includes(action); const heldPath = action === "options" ? "/host" : action === "submit" ? "/ticket" : `/ticket/71/${action}`; let hostPages = 0; let detailReads = 0; await boundary(page, async (route, path, entry) => { if (path === "/ticket/config") { await respond(route, { ticket_notice_open: 0 }); return true; } if (["/ticket/type", "/ticket/department"].includes(path)) { await respond(route, { list: [{ id: 1, name: "本地支持" }] }); return true; } if (path === "/ticket/71" && entry.method === "GET") { detailReads += 1; await respond(route, { ticket: { id: 71, title: "本地工单", content: "本地内容
", can_operate: 1, attachment: [], replies: [], create_time: 1700000000, }, }); return true; } if (path !== heldPath) return false; if (action === "options") hostPages += 1; started.resolve(); await release.promise; await respond( route, action === "options" ? { list: [{ id: 1, name: "本地实例" }], count: 2 } : { id: 71 }, ); return true; }); await login(page, detail ? "/tickets/71" : "/tickets/create"); if (action === "submit") { await page .getByPlaceholder("输入工单标题", { exact: true }) .fill("本地测试标题"); await page .getByRole("textbox", { name: "工单内容", exact: true }) .fill("本地测试内容"); await page .getByRole("button", { name: "创建工单", exact: true }) .click(); } if (detail) { await page .getByRole("heading", { name: "本地工单", exact: true }) .waitFor(); if (action === "reply") await page .getByRole("textbox", { name: "回复内容", exact: true }) .fill("本地测试回复"); await page .getByRole("button", { name: { reply: "提交回复", urge: "催单", close: "关闭工单" }[ action ], exact: true, }) .click(); } await started.promise; await page .getByRole("navigation", { name: "会员中心导航" }) .getByRole("link", { name: "SSH 密钥", exact: true }) .click(); await page.waitForURL((url) => url.pathname === "/security/ssh-keys"); await releaseResponse(page, release, heldPath); assert.equal( new URL(page.url()).pathname, "/security/ssh-keys", "Old ticket completion must not navigate the current page", ); if (action === "options") assert.equal( hostPages, 1, "Departed options loader cannot fetch the next host page", ); if (detail) assert.equal( detailReads, 1, "Departed ticket write cannot derive a detail reload", ); return { action, currentRoutePreserved: true, hostPages, detailReads }; }), ); } for (const outcome of ["success", "error"]) { test( `SSH save ${outcome} cannot own a reopened editor`, { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); let saves = 0; await boundary(page, async (route, path, entry) => { if (path !== "/ssh_key" || entry.method !== "POST") return false; saves += 1; if (saves === 1) { started.resolve(); await release.promise; } await respond( route, {}, saves === 1 && outcome === "error" ? 422 : 200, ); return true; }); await login(page); await page.getByText("audit-01", { exact: true }).waitFor(); await page .getByRole("button", { name: "添加密钥", exact: true }) .click(); let editor = page.getByRole("dialog", { name: "添加密钥", exact: true, }); await editor.getByLabel("名称 *", { exact: true }).fill("old-draft"); await editor .getByLabel("公钥内容 *", { exact: true }) .fill("ssh-ed25519 local-old-public-key"); await editor.getByRole("button", { name: "保存", exact: true }).click(); await started.promise; await editor.getByRole("button", { name: "取消", exact: true }).click(); await page .getByRole("button", { name: "添加密钥", exact: true }) .click(); editor = page.getByRole("dialog", { name: "添加密钥", exact: true }); await editor.getByLabel("名称 *", { exact: true }).fill("new-draft"); await editor .getByLabel("公钥内容 *", { exact: true }) .fill("ssh-ed25519 local-new-public-key"); await releaseResponse(page, release, "/ssh_key"); assert.equal( await editor.isVisible(), true, "Old completion cannot close the new editor", ); assert.equal( await editor.getByLabel("名称 *", { exact: true }).inputValue(), "new-draft", ); assert.equal( await editor.getByRole("alert").count(), 0, "Old failure cannot become the new draft's error", ); await editor.getByRole("button", { name: "保存", exact: true }).click(); await editor.waitFor({ state: "hidden" }); assert.equal(saves, 2, "The new editor remains saveable"); return { outcome, newerDraftPreserved: true, saves }; }), ); }