feat(web): enable UV editing in web UI and use OPFS save/reopen
- 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>
This commit is contained in:
90
web/tests/e2e/web-uv-workspace-gate.spec.ts
Normal file
90
web/tests/e2e/web-uv-workspace-gate.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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 选择");
|
||||
});
|
||||
Reference in New Issue
Block a user