- Forward workspace and mode to the native context in ?ui=web. - Locate the writable native UV area; enter Edit explicitly and wait for project recovery locks before switching mode. - Save projects to OPFS by default; add an explicit manual .blend download. - Add UV E2E coverage and implementation status. Co-Authored-By: Claude Code <noreply@anthropic.com>
91 lines
5.4 KiB
TypeScript
91 lines
5.4 KiB
TypeScript
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<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 ensureEditMode(page: import("@playwright/test").Page): Promise<void> {
|
||
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<void> {
|
||
// 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<void> {
|
||
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 选择");
|
||
});
|