import { clickXmindAction, openXmindStructures } from "./editor-actions"; import { expect, type Page, test } from "@playwright/test"; import JSZip from "jszip"; async function upload(page: Page, rootTopic: object, name: string) { const zip = new JSZip(); zip.file("content.json", JSON.stringify([{ title: name, rootTopic }])); await page.locator("input[accept*=xmind]").setInputFiles({ name: `${name}.xmind`, mimeType: "application/octet-stream", buffer: await zip.generateAsync({ type: "nodebuffer" }), }); } test("side-title tree table renders a spanning left header and preserves its native structure", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await page.goto("/dev/flow-board-preview", { waitUntil: "commit" }); await upload( page, { id: "root", title: "发布协调", structureClass: "org.xmind.ui.treetable", style: { properties: { "svg:fill": "#000000FF" } }, children: { attached: [ { id: "plan", title: "规划", style: { properties: { "svg:fill": "#F7C85FFF" } }, children: { attached: [ { id: "scope", title: "范围确认" }, { id: "date", title: "发布日期" }, ], }, }, { id: "ship", title: "交付" }, ], }, }, "侧标题树形表格" ); const canvas = page.locator(".drawnix"); const table = canvas.locator('[data-special-table="treeTableSide"]'); await expect(table).toBeVisible(); await expect(table.locator("[data-special-table-cell]")).toHaveCount(5); await expect( table.locator('[data-special-table-cell][data-depth="0"]') ).toHaveAttribute("fill", "#000000"); await clickXmindAction(page, "适应画布"); const box = async (title: string) => (await canvas.getByText(title, { exact: true }).boundingBox())!; const root = await box("发布协调"); const plan = await box("规划"); const scope = await box("范围确认"); const date = await box("发布日期"); expect(plan.x).toBeGreaterThan(root.x + root.width); expect(scope.x).toBeGreaterThan(plan.x + plan.width); expect(date.y).toBeGreaterThan(scope.y); await canvas.getByText("交付", { exact: true }).click(); await page.getByRole("button", { name: "子主题", exact: true }).click(); await page.keyboard.insertText("上线复盘"); await page .locator(".flow-board-canvas-shell") .click({ position: { x: 50, y: 100 } }); await expect(canvas.getByText("上线复盘", { exact: true })).toBeVisible(); await expect(table.locator("[data-special-table-cell]")).toHaveCount(6); await page.screenshot({ path: test.info().outputPath("side-tree-table-preview.png"), animations: "disabled", }); await canvas.getByText("发布协调", { exact: true }).click(); await openXmindStructures(page); await expect( page.getByRole("button", { name: "树形表格(侧标题)", exact: true }) ).toHaveAttribute("aria-pressed", "true"); const downloadPromise = page.waitForEvent("download"); await page.getByRole("button", { name: "导出 XMind", exact: true }).click(); const stream = await (await downloadPromise).createReadStream(); const chunks: Buffer[] = []; for await (const chunk of stream!) chunks.push(chunk); const exported = await JSZip.loadAsync(Buffer.concat(chunks)); const content = JSON.parse( await exported.file("content.json")!.async("string") ); expect(content[0].rootTopic.structureClass).toBe( "org.xmind.ui.treetable" ); expect(errors).toEqual([]); }); test("spreadsheet imports XMind columns, stacks labelled entries and round-trips the column model", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await page.goto("/dev/flow-board-preview", { waitUntil: "commit" }); const columns = ["Monday", "Tuesday", "Friday"]; await upload( page, { id: "root", title: "Mood Tracker", structureClass: "org.xmind.ui.spreadsheet", style: { properties: { "svg:fill": "#1F2766FF" } }, extensions: [ { provider: "org.xmind.ui.spreadsheet", content: [ { name: "columns", content: columns.map((column) => ({ name: "column", content: column, })), }, ], }, ], children: { attached: [ { id: "happy", title: "Happy", style: { properties: { "svg:fill": "#FF9D8EFF" } }, children: { attached: [ { id: "walk", title: "Morning walk", labels: ["Monday"] }, { id: "coffee", title: "Coffee", labels: ["Monday"] }, { id: "music", title: "Music", labels: ["Friday"] }, ], }, }, { id: "calm", title: "Calm", children: { attached: [ { id: "read", title: "Read", labels: ["Tuesday"] }, ], }, }, ], }, }, "矩阵" ); const canvas = page.locator(".drawnix"); const table = canvas.locator('[data-special-table="spreadsheet"]'); await expect(table).toBeVisible(); await expect(table.locator("[data-special-matrix-column]")).toHaveCount(3); await expect(table.locator("[data-special-matrix-cell]")).toHaveCount(6); await expect(table.locator("[data-special-matrix-entry]")).toHaveCount(4); for (const column of columns) await expect( table.locator(`[data-special-matrix-column-label="${column}"]`) ).toHaveText(column); await clickXmindAction(page, "适应画布"); const box = async (title: string) => (await canvas.getByText(title, { exact: true }).boundingBox())!; const happy = await box("Happy"); const walk = await box("Morning walk"); const coffee = await box("Coffee"); const music = await box("Music"); const read = await box("Read"); expect(Math.abs(walk.x + walk.width / 2 - (coffee.x + coffee.width / 2))).toBeLessThan(1); expect(coffee.y).toBeGreaterThan(walk.y); expect(music.x).toBeGreaterThan(walk.x); expect(read.x).toBeGreaterThan(walk.x); expect(read.y).toBeGreaterThan(happy.y); await canvas.getByText("Coffee", { exact: true }).dblclick(); await page.keyboard.press("ControlOrMeta+A"); await page.keyboard.insertText("Team coffee"); await page .locator(".flow-board-canvas-shell") .click({ position: { x: 50, y: 100 } }); await expect(canvas.getByText("Team coffee", { exact: true })).toBeVisible(); await canvas.getByText("Mood Tracker", { exact: true }).click(); await openXmindStructures(page); await expect( page.getByRole("complementary", { name: "格式面板" }).getByLabel("矩阵", { exact: true }) ).toHaveAttribute("aria-pressed", "true"); const downloadPromise = page.waitForEvent("download"); await page.getByRole("button", { name: "导出 XMind", exact: true }).click(); const stream = await (await downloadPromise).createReadStream(); const chunks: Buffer[] = []; for await (const chunk of stream!) chunks.push(chunk); const buffer = Buffer.concat(chunks); const exported = await JSZip.loadAsync(buffer); const content = JSON.parse( await exported.file("content.json")!.async("string") ); expect(content[0].rootTopic.structureClass).toBe( "org.xmind.ui.spreadsheet" ); expect( content[0].rootTopic.extensions[0].content[0].content.map( (column: { content: string }) => column.content ) ).toEqual(columns); expect( content[0].rootTopic.children.attached[0].children.attached.find( (topic: { title: string }) => topic.title === "Team coffee" ).labels ).toEqual(["Monday"]); await page.locator("input[accept*=xmind]").setInputFiles({ name: "矩阵往返.xmind", mimeType: "application/octet-stream", buffer, }); await expect(table).toBeVisible(); await expect(table.locator("[data-special-matrix-column]")).toHaveCount(3); await clickXmindAction(page, "适应画布"); await page.screenshot({ path: test.info().outputPath("spreadsheet-preview.png"), animations: "disabled", }); expect(errors).toEqual([]); });