import { expect, test } from "@playwright/test"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const STUDIO = path.resolve(HERE, "../../app/public/tutorial/pbr/studio.blend"); const GLB = path.resolve(HERE, "../../../re/猴头.glb"); test.use({ viewport: { width: 1600, height: 900 } }); async function openStudioAndMonkey(page: import("@playwright/test").Page): Promise { await page.goto("/?ui=web"); await page.locator("main.blender-app").waitFor({ timeout: 120_000 }); await page.locator('[data-testid="blend-file-input"]').setInputFiles(STUDIO); await page.getByText(/Engine: SceneIR/).first().waitFor({ timeout: 120_000 }); await expect.poll(async () => page.getByTestId("scene-stats").innerText(), { timeout: 120_000 }).toMatch(/Objects 6/); await page.locator('[data-testid="blend-file-input"]').setInputFiles(GLB); await expect.poll(async () => page.getByTestId("scene-stats").innerText(), { timeout: 120_000 }).toMatch(/Objects 7/); } async function openSpreadsheet(page: import("@playwright/test").Page): Promise { await page.locator(".workspace-tabs button", { hasText: "Geometry Nodes" }).evaluate((element) => (element as HTMLElement).click()); const sheet = page.getByTestId("spreadsheet-view"); await sheet.waitFor({ timeout: 60_000 }); return sheet; } test("Spreadsheet browses mesh data, filters columns and exports CSV without editing the Main", async ({ page }) => { test.setTimeout(300_000); await openStudioAndMonkey(page); const sheet = await openSpreadsheet(page); await expect(sheet).toHaveAttribute("data-source", "objects"); await expect.poll(async () => sheet.getAttribute("data-row-count"), { timeout: 30_000 }).toBe("7"); await expect.poll(async () => sheet.getByTestId("spreadsheet-row").count()).toBe(7); await sheet.getByLabel("数据源").selectOption("vertices"); await expect.poll(async () => Number(await sheet.getAttribute("data-row-count")), { timeout: 30_000 }).toBeGreaterThan(100); await expect(sheet.getByTestId("spreadsheet-table")).toBeVisible(); await expect.poll(async () => sheet.getByTestId("spreadsheet-row").count()).toBeGreaterThan(0); await sheet.getByTestId("spreadsheet-row").first().click(); await expect(sheet).toHaveAttribute("data-selected-rows", "1"); await sheet.getByRole("button", { name: "选择本页", exact: true }).click(); const pageRows = await sheet.getByTestId("spreadsheet-row").count(); await expect.poll(async () => Number(await sheet.getAttribute("data-selected-rows"))).toBe(pageRows); await sheet.getByLabel("过滤行").fill("no-such-value-4d7a"); await expect.poll(async () => sheet.getAttribute("data-row-count")).toBe("0"); await expect(sheet.getByText("没有匹配过滤条件的行。")).toBeVisible(); await sheet.getByLabel("过滤行").fill(""); await expect.poll(async () => Number(await sheet.getAttribute("data-row-count"))).toBeGreaterThan(pageRows); await sheet.getByLabel("每页行数").selectOption("50"); await expect.poll(async () => sheet.getByTestId("spreadsheet-row").count()).toBe(50); await sheet.getByRole("button", { name: "下一页", exact: true }).click(); await expect.poll(async () => sheet.getAttribute("data-page")).toBe("1"); const beforeColumns = (await sheet.getAttribute("data-visible-columns")) ?? ""; await sheet.locator("summary", { hasText: "列" }).click(); await sheet.getByLabel("显示列 index").uncheck(); await expect.poll(async () => sheet.getAttribute("data-visible-columns")).not.toContain("index"); const withoutIndex = (await sheet.getAttribute("data-visible-columns")) ?? ""; expect(withoutIndex.length).toBeLessThan(beforeColumns.length); await sheet.getByLabel("右移列 x").click(); const reordered = (await sheet.getAttribute("data-visible-columns")) ?? ""; expect(reordered).not.toBe(withoutIndex); expect(reordered.split(",").slice(0, 2)).toEqual(["y", "x"]); await sheet.getByLabel("显示列 index").check(); const downloadPromise = page.waitForEvent("download", { timeout: 30_000 }); await sheet.getByTestId("spreadsheet-export").click(); const download = await downloadPromise; expect(download.suggestedFilename()).toMatch(/^spreadsheet-vertices-.*\.csv$/); const downloaded = await download.path(); expect(downloaded).toBeTruthy(); const csv = await fs.readFile(downloaded!, "utf8"); const lines = csv.trim().split("\n"); expect(lines.length).toBeGreaterThan(pageRows); expect(lines[0].split(",")).toContain("x"); const before = Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0"); await page.waitForTimeout(400); const after = Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0"); expect(after).toBe(before); }); test("Spreadsheet reports mesh attributes and keeps working after an OPFS reopen", async ({ page }) => { test.setTimeout(360_000); await openStudioAndMonkey(page); const sheet = await openSpreadsheet(page); await sheet.getByLabel("数据源").selectOption("attributes"); await expect.poll(async () => Number(await sheet.getAttribute("data-row-count")), { timeout: 30_000 }).toBeGreaterThan(0); const attributeNames = await sheet.getByTestId("spreadsheet-row").allInnerTexts(); expect(attributeNames.join("\n")).toMatch(/position/); await sheet.getByLabel("数据源").selectOption("uv"); await expect.poll(async () => Number(await sheet.getAttribute("data-row-count")), { timeout: 30_000 }).toBeGreaterThan(0); await sheet.getByLabel("数据源").selectOption("faces"); await expect.poll(async () => Number(await sheet.getAttribute("data-row-count")), { timeout: 30_000 }).toBeGreaterThan(0); await sheet.getByLabel("数据源").selectOption("edges"); await expect.poll(async () => Number(await sheet.getAttribute("data-row-count")), { timeout: 30_000 }).toBeGreaterThan(0); const meshSelect = sheet.getByLabel("网格"); const options = await meshSelect.locator("option").count(); expect(options).toBeGreaterThan(1); await meshSelect.selectOption({ index: options - 1 }); await expect.poll(async () => sheet.getAttribute("data-mesh-id")).not.toBe(""); await page.locator('button[aria-label="保存项目"]').evaluate((element) => (element as HTMLElement).click()); await expect(page.locator("main.blender-app")).toHaveAttribute("data-user-action-save-status", "SUCCEEDED", { timeout: 60_000 }); await page.reload(); await page.locator("main.blender-app").waitFor({ timeout: 120_000 }); await expect.poll(async () => page.locator('[data-testid="recent-projects"] option').count(), { timeout: 60_000 }).toBeGreaterThan(1); await page.locator('[data-testid="recent-projects"]').selectOption({ index: 1 }); const discard = page.getByRole("button", { name: "不保存,继续", exact: true }); if (await discard.count()) await discard.click(); await expect.poll(async () => page.getByTestId("scene-stats").innerText(), { timeout: 120_000 }).toMatch(/Objects 7/); const reopened = await openSpreadsheet(page); await expect.poll(async () => reopened.getAttribute("data-row-count"), { timeout: 60_000 }).toBe("7"); await reopened.getByLabel("数据源").selectOption("vertices"); await expect.poll(async () => Number(await reopened.getAttribute("data-row-count")), { timeout: 60_000 }).toBeGreaterThan(100); });