import { chooseXmindLayout, clickXmindAction } from "./editor-actions"; import { expect, test } from "@playwright/test"; import JSZip from "jszip"; test("vertical timeline mirrors branches, edits children and survives sheet switching and file round-trip", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await page.goto("/dev/flow-board-preview", { waitUntil: "commit" }); const zip = new JSZip(); zip.file( "content.json", JSON.stringify([ { title: "纵向计划", rootTopic: { id: "root", title: "年度计划", structureClass: "org.xmind.ui.timeline.vertical", children: { attached: [ { id: "a", title: "第一阶段" }, { id: "b", title: "第二阶段", children: { attached: [{ id: "b1", title: "已有左侧任务" }] }, }, { id: "c", title: "第三阶段" }, { id: "d", title: "第四阶段" }, ], }, }, }, { title: "另一个画布", rootTopic: { id: "other", title: "备用中心" } }, ]) ); await page.locator("input[accept*=xmind]").setInputFiles({ name: "纵向计划.xmind", mimeType: "application/octet-stream", buffer: await zip.generateAsync({ type: "nodebuffer" }), }); const canvas = page.locator(".drawnix"); const axis = canvas.locator('[data-special-axis="verticalTimeline"]'); await expect(axis).toHaveCount(1); await clickXmindAction(page, "适应画布"); const box = async (title: string) => (await canvas.getByText(title, { exact: true }).boundingBox())!; const root = await box("年度计划"), first = await box("第一阶段"), second = await box("第二阶段"), third = await box("第三阶段"); expect(first.x).toBeGreaterThan(root.x + root.width / 2); expect(second.x + second.width).toBeLessThan(root.x + root.width / 2); expect(second.y).toBeGreaterThan(first.y + first.height); expect(third.y).toBeGreaterThan(second.y + second.height); expect((await box("已有左侧任务")).x).toBeLessThan(second.x); await canvas.getByText("第二阶段", { exact: true }).click(); await page.getByRole("button", { name: "子主题", exact: true }).click(); await expect(canvas.locator('[contenteditable="true"]')).toBeFocused(); await page.keyboard.insertText("新左侧任务"); await page .locator(".flow-board-canvas-shell") .click({ position: { x: 100, y: 100 } }); await expect(canvas.getByText("新左侧任务", { exact: true })).toBeVisible(); expect((await box("新左侧任务")).x).toBeLessThan((await box("第二阶段")).x); await canvas.getByText("年度计划", { exact: true }).click(); await chooseXmindLayout(page, "向右逻辑"); await expect(axis).toHaveCount(0); await clickXmindAction(page, "撤销"); await expect(axis).toHaveCount(1); await page.getByRole("button", { name: "另一个画布", exact: true }).click(); await expect(axis).toHaveCount(0); await page.getByRole("button", { name: "纵向计划", exact: true }).click(); await expect(axis).toHaveCount(1); const downloading = page.waitForEvent("download"); await page.getByRole("button", { name: "导出 XMind", exact: true }).click(); const stream = await (await downloading).createReadStream(); const chunks = []; for await (const chunk of stream!) chunks.push(chunk); const buffer = Buffer.concat(chunks); const exported = await JSZip.loadAsync(buffer); const sheets = JSON.parse( await exported.file("content.json")!.async("string") ); expect(sheets[0].rootTopic.structureClass).toBe( "org.xmind.ui.timeline.vertical" ); expect( sheets[0].rootTopic.children.attached[1].children.attached.map( (t: { title: string }) => t.title ) ).toContain("新左侧任务"); await page .locator("input[accept*=xmind]") .setInputFiles({ name: "往返.xmind", mimeType: "application/octet-stream", buffer, }); await expect(axis).toHaveCount(1); await expect(canvas.getByText("新左侧任务", { exact: true })).toBeVisible(); await clickXmindAction(page, "适应画布"); await page.screenshot({ path: test.info().outputPath("vertical-timeline-preview.png"), animations: "disabled", }); expect(errors).toEqual([]); });