import { expect, test, type Page } from "@playwright/test"; test.setTimeout(60_000); const projectId = "8d3b4bac-4539-4caf-8b49-b063f218de19"; const imageOrigin = "https://scqnycdn.jiansun.vip"; const imageNode = (id: string) => ({ id, type: "image", position: { x: 160, y: 130 }, size: { width: 300, height: 200 }, data: { status: "completed", src: `${imageOrigin}/uploads/${id}.png`, title: id } }); async function setup(page: Page, failRead = false) { const state = { canvasData: { nodes: [imageNode("source")], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: "select" }, revision: 1, failSave: false, failRead, conflictNextSave: false, conflictSameNode: false, writes: [] as any[], metadataWrites: 0, submissions: [] as any[], capabilitySubmissions: [] as any[], appVersion: "development", holdRequest: false, finishRequest: null as (() => void) | null, nativeResultNext: false, }; const revision = () => new Date(Date.UTC(2026, 10, 6, 0, 0, state.revision)).toISOString(); const project = () => ({ id: projectId, title: "同步验证", canvasData: state.canvasData, updatedAt: revision() }); const errors: string[] = []; page.on("pageerror", error => errors.push(error.message)); await page.context().addInitScript(() => localStorage.setItem("theme", "light")); await page.context().route("**/api/**", async route => { const request = route.request(); const path = new URL(request.url()).pathname; let data: unknown = {}; let status = 200; if (path === `/api/canvas/${projectId}`) { if (request.method() === "GET") { status = state.failRead ? 500 : 200; data = state.failRead ? { success: false, error: "画布暂时无法读取,现有内容未被覆盖。" } : { success: true, data: project() }; } else if (request.method() === "PUT") { const body = request.postDataJSON(); state.writes.push(body); const previousCanvasRevision = revision(); if (body.canvasData) { if (state.conflictNextSave) { state.conflictNextSave = false; if (state.conflictSameNode) state.canvasData.nodes[0].position.x = 500; else state.canvasData.nodes.push(imageNode("remote-result")); state.revision++; } if (state.failSave) { status = 503; data = { success: false, error: "同步服务暂时不可用" }; } else if ((body.baseCanvasUpdatedAt || body.baseCanvasRevision) !== revision()) { status = 409; data = { success: false, code: "STALE_CANVAS_REVISION", error: "画布已更新", canvasUpdatedAt: revision() }; } else { state.canvasData = body.canvasData; state.revision++; data = { success: true, previousCanvasRevision, data: project() }; } } else { state.metadataWrites++; state.revision++; data = { success: true, previousCanvasRevision, data: project() }; } } } else if (path === `/api/canvas/${projectId}/capabilities` && request.method() === "POST") { state.capabilitySubmissions.push(request.postDataJSON()); state.canvasData.nodes.push({ ...imageNode(`enhanced-${state.capabilitySubmissions.length}`), position: { x: 520, y: 130 } }); state.revision++; data = { success: true, task: { status: "succeeded" } }; } else if (path === "/api/canvas/agent-chat" && request.method() === "POST") { state.submissions.push(request.postDataJSON()); let operation = ""; if (state.nativeResultNext) { state.nativeResultNext = false; state.canvasData.nodes.push({ ...imageNode("agent-result"), position: { x: 520, y: 130 } }); state.revision++; operation = `data: ${JSON.stringify({ type: "data-canvasOperation", data: { status: "applied", projectId, createdNodeIds: ["agent-result"] } })}\n\n`; } if (state.holdRequest) await new Promise(resolve => { state.finishRequest = resolve; }); await route.fulfill({ headers: { "content-type": "text/event-stream", "x-vercel-ai-ui-message-stream": "v1" }, body: 'data: {"type":"start","messageId":"sync-assistant"}\n\n' + operation + 'data: {"type":"text-start","id":"text"}\n\ndata: {"type":"text-delta","id":"text","delta":"已收到请求"}\n\ndata: {"type":"text-end","id":"text"}\n\ndata: {"type":"finish"}\n\ndata: [DONE]\n\n' }); return; } else if (path === "/api/app-version") data = { version: state.appVersion }; else if (path === "/api/files/direct-upload") { data = request.postDataJSON().action === "complete" ? { url: `${imageOrigin}/uploads/uploaded.png`, pathname: "uploads/uploaded.png" } : { mode: "completed", key: "uploads/uploaded.png" }; } else if (path === "/api/files/upload") data = { url: `${imageOrigin}/uploads/uploaded.png` }; else if (path === "/api/canvas/auto-title") data = { title: "参考图片" }; else if (path.includes("validate-session")) data = { valid: true, success: true }; else if (path === "/api/auth/session") data = { user: { id: "sync-user", name: "同步验证", type: "admin" }, expires: "2099-01-01" }; else if (path === "/api/site-settings") data = { canvasEnabled: true, agentEnabled: true, membershipEnabled: false }; else if (path === "/api/models") data = { models: [{ id: "image-test", modelId: "gpt-image-2", providerType: "openai", name: "GPT Image", type: "image", enabled: true, supportsImageGen: true }] }; else if (path === "/api/vote") data = []; else if (path.includes("messages")) data = { messages: [] }; else if (path.includes("sessions")) data = { sessions: [] }; else if (path.startsWith("/api/canvas/")) data = { success: true, runs: [] }; await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(data) }); }); await page.context().route(`${imageOrigin}/**`, route => route.fulfill({ contentType: "image/svg+xml", body: '' })); await page.goto(`/canvas/${projectId}`, { waitUntil: "domcontentloaded" }); if (!failRead) await expect(page.getByRole("heading", { name: /Hi, 同步验证/ })).toBeVisible(); return { state, errors }; } async function moveImage(page: Page) { const node = page.locator('[data-canvas-node-id="source"]'); await expect(node).toBeVisible(); const box = (await node.boundingBox())!; await page.mouse.move(box.x + 100, box.y + 80); await page.mouse.down(); await page.mouse.move(box.x + 190, box.y + 120, { steps: 5 }); await page.mouse.up(); } const unloadWouldBeBlocked = (page: Page) => page.evaluate(() => { const event = new Event("beforeunload", { cancelable: true }); window.dispatchEvent(event); return event.defaultPrevented; }); test("background saving recovers when the network returns without a manual save", async ({ page }) => { const { state, errors } = await setup(page); await expect(page.getByRole("button", { name: "保存", exact: true })).toHaveCount(0); state.failSave = true; await moveImage(page); await expect.poll(() => state.writes.filter(body => body.canvasData).length).toBeGreaterThan(0); expect(state.canvasData.nodes[0].position.x).toBe(160); state.failSave = false; await page.evaluate(() => window.dispatchEvent(new Event("online"))); await expect.poll(() => state.canvasData.nodes[0].position.x).toBeGreaterThan(160); await expect.poll(() => unloadWouldBeBlocked(page)).toBe(false); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("only unsaved content blocks closing and beforeunload does not issue duplicate saves", async ({ page }) => { const { state, errors } = await setup(page); expect(await unloadWouldBeBlocked(page)).toBe(false); await page.locator('[data-canvas-node-id="source"]').click(); expect(await unloadWouldBeBlocked(page)).toBe(false); state.failSave = true; await moveImage(page); const writes = state.writes.length; expect(await unloadWouldBeBlocked(page)).toBe(true); expect(state.writes).toHaveLength(writes); const draft = await page.evaluate(id => localStorage.getItem(`canvas-unsynced:${id}`), projectId); expect(JSON.parse(JSON.parse(draft!).snapshot).canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("pagehide and visibilitychange issue only one closing save", async ({ page }) => { const { state, errors } = await setup(page); state.failSave = true; await moveImage(page); const writes = state.writes.length; await page.evaluate(() => { window.dispatchEvent(new Event("pagehide")); Object.defineProperty(document, "visibilityState", { value: "hidden", configurable: true }); document.dispatchEvent(new Event("visibilitychange")); }); await expect.poll(() => state.writes.length).toBe(writes + 1); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("leaving with pending changes can continue editing or save before navigating", async ({ page }) => { const { state, errors } = await setup(page); await page.route(url => url.pathname === "/", route => route.fulfill({ contentType: "text/html; charset=utf-8", body: "

已返回首页

" })); await moveImage(page); await page.getByRole("button", { name: "返回首页", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "离开前保存画布?" }); await expect(dialog).toBeVisible(); await page.screenshot({ path: test.info().outputPath("leave-dialog-desktop.png"), animations: "disabled" }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: test.info().outputPath("leave-dialog-mobile.png"), animations: "disabled" }); await page.setViewportSize({ width: 1440, height: 900 }); await dialog.getByRole("button", { name: "继续编辑", exact: true }).click(); await expect(dialog).toHaveCount(0); await moveImage(page); await page.getByRole("button", { name: "返回首页", exact: true }).click(); await dialog.getByRole("button", { name: "保存并离开", exact: true }).click(); await expect(page.getByText("已返回首页")).toBeVisible(); expect(state.canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("failed saving keeps the leave dialog and canvas until a successful retry", async ({ page }) => { const { state, errors } = await setup(page); await page.route(url => url.pathname === "/", route => route.fulfill({ contentType: "text/html; charset=utf-8", body: "

已返回首页

" })); state.failSave = true; await moveImage(page); await page.getByRole("button", { name: "返回首页", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "离开前保存画布?" }); await dialog.getByRole("button", { name: "保存并离开", exact: true }).click(); await expect(dialog.getByRole("alert")).toContainText("HTTP 503"); await expect(page.locator('[data-canvas-node-id="source"]')).toBeVisible(); expect(state.submissions).toHaveLength(0); state.failSave = false; await dialog.getByRole("button", { name: "保存并离开", exact: true }).click(); await expect(page.getByText("已返回首页")).toBeVisible(); expect(state.canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(errors).toEqual([]); }); test("discarding pending changes leaves without saving or restoring the discarded draft", async ({ page }) => { const { state, errors } = await setup(page); await page.route(url => url.pathname === "/", route => route.fulfill({ contentType: "text/html; charset=utf-8", body: "

已返回首页

" })); state.failSave = true; await moveImage(page); expect(await unloadWouldBeBlocked(page)).toBe(true); const writes = state.writes.length; await page.getByRole("button", { name: "返回首页", exact: true }).click(); await page.getByRole("button", { name: "不保存并离开", exact: true }).click(); await expect(page.getByText("已返回首页")).toBeVisible(); expect(state.writes).toHaveLength(writes); expect(state.canvasData.nodes[0].position.x).toBe(160); expect(await page.evaluate(id => localStorage.getItem(`canvas-unsynced:${id}`), projectId)).toBeNull(); expect(errors).toEqual([]); }); test("creating another project also offers to preserve pending canvas changes", async ({ page }) => { const { state, errors } = await setup(page); state.failSave = true; await moveImage(page); await page.getByRole("button", { name: "查看项目文件", exact: true }).click(); await page.getByRole("button", { name: "新建画布", exact: true }).first().click(); await expect(page.getByRole("dialog", { name: "离开前保存画布?" })).toBeVisible(); await page.getByRole("button", { name: "继续编辑", exact: true }).click(); await expect(page).toHaveURL(new RegExp(`/canvas/${projectId}$`)); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("an enhanced result can be moved and archived before the next save without blocking Agent", async ({ page }) => { const { state, errors } = await setup(page); await page.locator('[data-canvas-node-id="source"]').click(); await page.locator('[data-tool-key="enhance"]').click(); const result = page.locator('[data-canvas-node-id="enhanced-1"]'); await expect(result).toBeVisible(); const box = (await result.boundingBox())!; await page.mouse.move(box.x + 100, box.y + 80); await page.mouse.down(); await page.mouse.move(box.x + 165, box.y + 110, { steps: 5 }); await page.mouse.up(); state.canvasData.nodes.find(node => node.id === "enhanced-1")!.data.src = `${imageOrigin}/uploads/archived-result.png`; state.revision++; const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("保留增强结果的位置,继续分析"); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(1); const saved = state.canvasData.nodes.find(node => node.id === "enhanced-1")!; expect(saved.position.x).toBeGreaterThan(520); expect(saved.data.src).toContain("archived-result.png"); expect(state.capabilitySubmissions).toHaveLength(1); await page.reload(); await expect(page.locator('[data-canvas-node-id="enhanced-1"]')).toBeVisible(); expect(errors).toEqual([]); }); test("toolbar save failures show the cause and recover without a paid submission", async ({ page }) => { const { state, errors } = await setup(page); await moveImage(page); state.failSave = true; await page.locator('[data-tool-key="enhance"]').click(); await expect(page.getByText(/画布自动同步失败(HTTP 503)/)).toBeVisible(); expect(state.capabilitySubmissions).toHaveLength(0); state.failSave = false; await page.locator('[data-tool-key="enhance"]').click(); await expect(page.locator('[data-canvas-node-id="enhanced-1"]')).toBeVisible(); expect(state.capabilitySubmissions).toHaveLength(1); expect(errors).toEqual([]); }); test("native Agent results update the save baseline before moving and archiving the result", async ({ page }) => { const { state, errors } = await setup(page); state.nativeResultNext = true; const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("先分析再处理图片"); await editor.press("Enter"); const result = page.locator('[data-canvas-node-id="agent-result"]'); await expect(result).toBeVisible(); await expect(editor).toBeEditable(); const box = (await result.boundingBox())!; await page.mouse.move(box.x + 100, box.y + 80); await page.mouse.down(); await page.mouse.move(box.x + 165, box.y + 110, { steps: 5 }); await page.mouse.up(); state.canvasData.nodes.find(node => node.id === "agent-result")!.data.src = `${imageOrigin}/uploads/agent-archived.png`; state.revision++; await editor.fill("保留位置,继续分析"); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(2); const saved = state.canvasData.nodes.find(node => node.id === "agent-result")!; expect(saved.position.x).toBeGreaterThan(520); expect(saved.data.src).toContain("agent-archived.png"); expect(state.capabilitySubmissions).toHaveLength(0); expect(errors).toEqual([]); }); test("upload naming and consecutive Agent messages sync automatically without manual saves", async ({ page }) => { const { state, errors } = await setup(page); const editor = page.getByLabel("描述你的设计需求", { exact: true }); await expect(editor).toBeVisible(); await editor.evaluate(element => { const transfer = new DataTransfer(); transfer.items.add(new File([new Uint8Array([137, 80, 78, 71])], "参考图.png", { type: "image/png" })); element.dispatchEvent(new ClipboardEvent("paste", { clipboardData: transfer, bubbles: true, cancelable: true })); }); await expect.poll(() => state.metadataWrites).toBe(1); expect(state.canvasData.nodes).toHaveLength(2); for (const prompt of ["请先分析参考图", "继续调整为白色背景"]) { await editor.fill(prompt); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(prompt.startsWith("请") ? 1 : 2); await expect(editor).toBeEditable(); } await page.reload(); await expect(page.locator('[data-canvas-node-id]')).toHaveCount(2); expect(errors).toEqual([]); }); test("a save conflict merges a remote result with local movement before one Agent submission", async ({ page }) => { const { state, errors } = await setup(page); await moveImage(page); state.conflictNextSave = true; const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("保留我移动后的位置,再分析图片"); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(1); expect(state.canvasData.nodes.map(node => node.id)).toEqual(["source", "remote-result"]); expect(state.canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(errors).toEqual([]); }); test("failed synchronization preserves the unsent message and canvas draft across refresh", async ({ page }) => { const { state, errors } = await setup(page); await moveImage(page); state.failSave = true; const editor = page.getByLabel("描述你的设计需求", { exact: true }); const prompt = "保持产品结构,只更换背景"; await editor.fill(prompt); await editor.press("Enter"); await expect(page.getByText(/本地草稿已保留/).first()).toBeVisible(); expect(state.submissions).toHaveLength(0); await expect(editor).toHaveText(prompt); await page.reload(); await expect(editor).toHaveText(prompt); await expect(page.locator('[data-canvas-node-id="source"]')).toBeVisible(); await expect(page.getByRole("heading", { name: /Hi, 同步验证/ })).toBeVisible(); state.failSave = false; await expect(page.getByRole("button", { name: "发送", exact: true })).toBeEnabled(); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(1); expect(state.canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(errors).toEqual([]); }); test("conflicting edits keep both canvases and do not submit an Agent task", async ({ page }) => { const { state, errors } = await setup(page); await moveImage(page); state.conflictSameNode = true; state.conflictNextSave = true; const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("请处理这张图片"); await editor.press("Enter"); await expect(page.getByText(/画布同步发生冲突/).first()).toBeVisible(); expect(state.submissions).toHaveLength(0); expect(state.canvasData.nodes[0].position.x).toBe(500); const draft = await page.evaluate(id => JSON.parse(localStorage.getItem(`canvas-unsynced:${id}`) || "null"), projectId); expect(JSON.parse(draft.snapshot).canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(JSON.parse(draft.snapshot).canvasData.nodes[0].position.x).not.toBe(500); await expect(editor).toHaveText("请处理这张图片"); expect(errors).toEqual([]); }); for (const choice of ["local", "remote"] as const) { test(`an existing conflicting draft can keep the ${choice} edit and resume without losing other images`, async ({ page }) => { const { state, errors } = await setup(page); await moveImage(page); state.conflictSameNode = true; state.conflictNextSave = true; const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("继续分析图片"); await editor.press("Enter"); await expect(page.getByRole("button", { name: "保留本页修改", exact: true })).toBeVisible(); state.canvasData.nodes.push(imageNode("remote-addition")); state.revision++; await page.reload(); await expect(page.locator('[data-canvas-node-id="remote-addition"]')).toBeVisible(); if (choice === "local") { await page.screenshot({ path: test.info().outputPath("conflict-recovery-desktop.png"), animations: "disabled" }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: test.info().outputPath("conflict-recovery-mobile.png"), animations: "disabled" }); await page.setViewportSize({ width: 1440, height: 900 }); } await page.getByRole("button", { name: choice === "local" ? "保留本页修改" : "保留云端修改", exact: true }).click(); await expect(page.getByRole("button", { name: "保留本页修改", exact: true })).toHaveCount(0); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(1); const x = state.canvasData.nodes.find(node => node.id === "source")!.position.x; if (choice === "remote") expect(x).toBe(500); else { expect(x).toBeGreaterThan(160); expect(x).not.toBe(500); } expect(state.canvasData.nodes.map(node => node.id)).toEqual(["source", "remote-addition"]); expect(state.capabilitySubmissions).toHaveLength(0); expect(errors).toEqual([]); }); } test("a failed canvas read cannot create or save an empty replacement", async ({ page }) => { const { state, errors } = await setup(page, true); await expect(page.getByRole("alert").filter({ hasText: "画布暂时无法读取" })).toBeVisible(); await page.reload(); await expect(page.getByRole("alert").filter({ hasText: "画布暂时无法读取" })).toBeVisible(); expect(state.writes).toHaveLength(0); state.failRead = false; await page.getByRole("button", { name: "重新加载画布" }).click(); await expect(page.locator('[data-canvas-node-id="source"]')).toBeVisible(); expect(state.canvasData.nodes).toHaveLength(1); expect(errors).toEqual([]); }); async function showUpdate(page: Page, state: { appVersion: string }) { state.appVersion = "20261106T120000Z-nextrelease1"; await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await expect(page.getByRole("alertdialog")).toContainText("平台已更新"); } async function countRefreshes(page: Page, state: { appVersion: string }) { let navigations = 0; await page.route(url => url.pathname === `/canvas/${projectId}`, route => { if (route.request().isNavigationRequest()) { navigations++; state.appVersion = "development"; } return route.continue(); }); return () => navigations; } test("an older browser without AbortSignal static helpers receives the release notice", async ({ page }) => { await page.context().addInitScript(() => { Object.defineProperty(AbortSignal, "any", { value: undefined, configurable: true }); Object.defineProperty(AbortSignal, "timeout", { value: undefined, configurable: true }); }); const { state, errors } = await setup(page); await showUpdate(page, state); await expect(page.getByRole("button", { name: "同步并刷新", exact: true })).toBeVisible(); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("a release is detected by the minute timer without requiring a focus change", async ({ page }) => { await page.clock.install(); const { state, errors } = await setup(page); state.appVersion = "20261106T120000Z-nextrelease1"; await page.clock.fastForward(60_001); await expect(page.getByRole("alertdialog")).toContainText("平台已更新"); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("a new release requires confirmation and refreshes after preserving canvas and input", async ({ page }) => { const { state, errors } = await setup(page); const refreshes = await countRefreshes(page, state); await moveImage(page); const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("刷新后还要保留这条输入"); await showUpdate(page, state); await page.keyboard.press("Escape"); await expect(page.getByRole("alertdialog")).toBeVisible(); expect(refreshes()).toBe(0); await page.screenshot({ path: test.info().outputPath("update-notice-desktop.png"), animations: "disabled" }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: test.info().outputPath("update-notice-mobile.png"), animations: "disabled" }); await page.getByRole("button", { name: "同步并刷新", exact: true }).click(); await expect.poll(refreshes).toBe(1); await expect(editor).toHaveText("刷新后还要保留这条输入"); await expect(page.getByRole("alertdialog")).toHaveCount(0); expect(state.canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(state.submissions).toHaveLength(0); expect(errors).toEqual([]); }); test("an update cannot interrupt or resubmit a running Agent task", async ({ page }) => { const { state, errors } = await setup(page); const refreshes = await countRefreshes(page, state); state.holdRequest = true; const editor = page.getByLabel("描述你的设计需求", { exact: true }); await editor.fill("先分析这张图片"); await editor.press("Enter"); await expect.poll(() => state.submissions.length).toBe(1); await showUpdate(page, state); const saves = state.writes.length; await page.getByRole("button", { name: "同步并刷新", exact: true }).click(); await expect(page.getByRole("alertdialog")).toContainText("当前对话任务仍在处理中"); expect(refreshes()).toBe(0); expect(state.writes).toHaveLength(saves); state.finishRequest?.(); await expect(editor).toBeEditable(); await page.getByRole("button", { name: "同步并刷新", exact: true }).click(); await expect.poll(refreshes).toBe(1); expect(state.submissions).toHaveLength(1); expect(errors).toEqual([]); }); test("an update keeps the current page when automatic synchronization fails", async ({ page }) => { const { state, errors } = await setup(page); const refreshes = await countRefreshes(page, state); state.failSave = true; await moveImage(page); await showUpdate(page, state); await page.getByRole("button", { name: "同步并刷新", exact: true }).click(); await expect(page.getByRole("alertdialog")).toContainText("HTTP 503"); expect(refreshes()).toBe(0); expect(state.submissions).toHaveLength(0); state.failSave = false; await page.getByRole("button", { name: "同步并刷新", exact: true }).click(); await expect.poll(refreshes).toBe(1); expect(state.canvasData.nodes[0].position.x).toBeGreaterThan(160); expect(errors).toEqual([]); }); test("a release refresh never saves an empty canvas after its initial read failed", async ({ page }) => { const { state, errors } = await setup(page, true); const refreshes = await countRefreshes(page, state); await expect(page.getByRole("alert").filter({ hasText: "画布暂时无法读取" })).toBeVisible(); await showUpdate(page, state); state.failRead = false; await page.getByRole("button", { name: "同步并刷新", exact: true }).click(); await expect.poll(refreshes).toBe(1); await expect(page.locator('[data-canvas-node-id="source"]')).toBeVisible(); expect(state.canvasData.nodes).toHaveLength(1); expect(state.writes.filter(body => body.canvasData?.nodes.length === 0)).toHaveLength(0); expect(errors).toEqual([]); });