Geometry Nodes workspace now exposes real mesh buffers (objects, vertices, edges, faces, UVs, attributes) with filtering, column show/hide/reorder, row selection, pagination and explicit CSV export. Interactions never touch the Main revision. Co-Authored-By: Claude Code <noreply@anthropic.com>
124 lines
7.3 KiB
TypeScript
124 lines
7.3 KiB
TypeScript
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<void> {
|
|
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<import("@playwright/test").Locator> {
|
|
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);
|
|
});
|