import { expect, test } from "@playwright/test"; 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 ensureEditMode(page: import("@playwright/test").Page): Promise { const status = page.locator('[data-testid="engine-status"]'); if ((await status.innerText()).includes("模式:Edit")) return; try { await expect(status).toContainText("模式:Edit", { timeout: 5_000 }); return; } catch { /* fall through to the explicit mode menu */ } await page.locator(".mode-chip button").first().evaluate((element) => (element as HTMLElement).click()); const editMode = page.getByRole("menuitemcheckbox", { name: /编辑模式/ }); await editMode.waitFor({ timeout: 15_000 }); await editMode.evaluate((element) => (element as HTMLElement).click()); await expect(status).toContainText("模式:Edit", { timeout: 60_000 }); await page.keyboard.press("Escape"); } async function openUVWorkspace(page: import("@playwright/test").Page): Promise { // The native desktop canvas can sit above the workspace tab at the same // coordinates in headed Chromium; invoke the tab's DOM click directly. await page.locator(".workspace-tabs button", { hasText: "UV Editing" }).evaluate((element) => (element as HTMLElement).click()); await expect(page.locator(".wasm-uv-editor")).toBeVisible({ timeout: 60_000 }); await expect(page.locator(".uv-controls")).toBeVisible({ timeout: 60_000 }); await expect.poll(async () => page.locator('[data-testid="editor-workflow-context"]').innerText(), { timeout: 60_000 }) .toContain("UV Editing"); // Desktop Blender's UV editor requires OB_MODE_EDIT (space_image.cc); // enter Edit explicitly rather than depending on a startup timing quirk. await ensureEditMode(page); await expect.poll(async () => page.locator(".wasm-uv-editor").innerText(), { timeout: 60_000 }).toContain("原生 UV 选择"); } async function selectAllAndUnwrap(page: import("@playwright/test").Page): Promise { await page.locator(".wasm-uv-editor button", { hasText: "全选 UV" }).evaluate((element) => (element as HTMLElement).click()); await expect.poll(async () => page.locator(".wasm-uv-editor").innerText(), { timeout: 60_000 }).toContain("UV 选择已提交"); await page.locator(".wasm-uv-editor button", { hasText: "展开所选 UV" }).evaluate((element) => (element as HTMLElement).click()); await expect.poll(async () => page.locator(".wasm-uv-editor").innerText(), { timeout: 60_000 }).toContain("展开 UV已提交"); } test("UV Editing finds a writable native UV area and commits an unwrap", async ({ page }) => { test.setTimeout(300_000); await openStudioAndMonkey(page); await openUVWorkspace(page); await expect(page.locator('[data-testid="engine-status"]')).toContainText("模式:Edit", { timeout: 60_000 }); const before = Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0"); await selectAllAndUnwrap(page); const after = Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0"); expect(after).toBeGreaterThan(before); }); test("UV workspace survives save and reopen", async ({ page }) => { test.setTimeout(360_000); await openStudioAndMonkey(page); await openUVWorkspace(page); await selectAllAndUnwrap(page); // Save through the OPFS project transaction (the download is a side effect // of the same commit), then reload and reopen the recent OPFS project. 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/); await openUVWorkspace(page); await expect(page.locator(".uv-controls")).toBeVisible({ timeout: 60_000 }); await expect.poll(async () => page.locator(".wasm-uv-editor").innerText(), { timeout: 60_000 }).toContain("原生 UV 选择"); });