A single-page 28-chapter manual generated from manual.json covers the 11 workspaces, all React editors, OPFS save/reopen, explicit local downloads, import/export routes, preferences/keymap and the capability matrix. It reuses verified course screenshots and adds 14 fresh Chromium captures for shell, storage, area, camera, preferences, IO and troubleshooting chapters. Manual-driven fixes: Save As now states OPFS-only, GLTF import is restored, quick toolbar honors its preference, startup workspace is applied, Properties tabs jump to sections, Outliner fallback search filters, and the editor-header ⋮ opens the area menu. Help and the tutorial landing link the new manual; the PDF remains as an archive. Co-Authored-By: Claude Code <noreply@anthropic.com>
87 lines
5.2 KiB
TypeScript
87 lines
5.2 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");
|
|
|
|
test.use({ viewport: { width: 1600, height: 900 } });
|
|
|
|
async function openStudio(page: import("@playwright/test").Page): Promise<import("@playwright/test").Locator> {
|
|
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 expect.poll(async () => page.getByTestId("scene-stats").innerText(), { timeout: 120_000 }).toMatch(/Objects 6/);
|
|
await page.waitForTimeout(1500);
|
|
const canvas = page.locator(".viewport-area canvas.viewport-canvas").first();
|
|
await canvas.waitFor({ timeout: 60_000 });
|
|
await canvas.focus();
|
|
await page.waitForTimeout(300);
|
|
return canvas;
|
|
}
|
|
|
|
async function dragMiddle(page: import("@playwright/test").Page, canvas: import("@playwright/test").Locator): Promise<void> {
|
|
const box = await canvas.boundingBox();
|
|
if (!box) throw new Error("viewport has no box");
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.down({ button: "middle" });
|
|
await page.mouse.move(box.x + box.width / 2 + 50, box.y + box.height / 2 + 25, { steps: 10 });
|
|
await page.mouse.up({ button: "middle" });
|
|
await page.waitForTimeout(700);
|
|
}
|
|
|
|
test("Camera view aligns to the scene camera and restores the last view", async ({ page }) => {
|
|
test.setTimeout(300_000);
|
|
const canvas = await openStudio(page);
|
|
await expect(page.locator("main.blender-app")).toHaveAttribute("data-project-snapshot-revision", /^[0-9]+$/);
|
|
const before = {
|
|
position: await canvas.getAttribute("data-camera-position"),
|
|
yaw: await canvas.getAttribute("data-camera-yaw"),
|
|
pitch: await canvas.getAttribute("data-camera-pitch"),
|
|
revision: await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision"),
|
|
};
|
|
await page.keyboard.press("Numpad0");
|
|
await expect(canvas).toHaveAttribute("data-camera-view", "CAMERA", { timeout: 30_000 });
|
|
await expect.poll(async () => canvas.getAttribute("data-camera-position"), { timeout: 30_000 }).toBe("7.2,-7.2,5.4");
|
|
await expect(page.locator("main.blender-app")).toHaveAttribute("data-project-snapshot-revision", before.revision ?? "0");
|
|
|
|
await dragMiddle(page, canvas);
|
|
await expect(canvas).toHaveAttribute("data-camera-view", "CAMERA");
|
|
await expect(page.locator("main.blender-app")).toHaveAttribute("data-project-snapshot-revision", before.revision ?? "0");
|
|
|
|
const zoomBefore = await canvas.getAttribute("data-camera-zoom");
|
|
await canvas.evaluate((element) => element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -120 })));
|
|
await expect.poll(async () => canvas.getAttribute("data-camera-zoom"), { timeout: 10_000 }).not.toBe(zoomBefore);
|
|
await expect(page.locator("main.blender-app")).toHaveAttribute("data-project-snapshot-revision", before.revision ?? "0");
|
|
|
|
await page.keyboard.press("Numpad0");
|
|
await expect(canvas).toHaveAttribute("data-camera-view", "USER", { timeout: 30_000 });
|
|
await expect.poll(async () => canvas.getAttribute("data-camera-position"), { timeout: 30_000 }).toBe(before.position);
|
|
await expect(canvas).toHaveAttribute("data-camera-yaw", before.yaw ?? "");
|
|
await expect(canvas).toHaveAttribute("data-camera-pitch", before.pitch ?? "");
|
|
});
|
|
|
|
test("Camera to View writes the camera and Camera to View lock moves it", async ({ page }) => {
|
|
test.setTimeout(300_000);
|
|
const canvas = await openStudio(page);
|
|
const before = Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0");
|
|
await page.keyboard.press("Control+Alt+Numpad0");
|
|
await expect.poll(async () => Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0"), { timeout: 60_000 }).toBeGreaterThan(before);
|
|
await expect(canvas).toHaveAttribute("data-camera-view", "CAMERA", { timeout: 30_000 });
|
|
await page.getByRole("button", { name: "锁定相机", exact: true }).evaluate((element) => (element as HTMLElement).click());
|
|
await expect(canvas).toHaveAttribute("data-camera-locked", "true");
|
|
const lockedRevision = Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0");
|
|
await dragMiddle(page, canvas);
|
|
await expect.poll(async () => Number(await page.locator("main.blender-app").getAttribute("data-project-snapshot-revision") ?? "0"), { timeout: 60_000 }).toBeGreaterThan(lockedRevision);
|
|
await expect(canvas).toHaveAttribute("data-camera-view", "CAMERA");
|
|
|
|
const otherCanvas = page.locator(".camera-view-area canvas.viewport-canvas").first();
|
|
if (await otherCanvas.count()) await expect(otherCanvas).not.toHaveAttribute("data-camera-locked", "true");
|
|
|
|
await page.reload();
|
|
await page.locator("main.blender-app").waitFor({ timeout: 120_000 });
|
|
const reloaded = page.locator(".viewport-area canvas.viewport-canvas").first();
|
|
await reloaded.waitFor({ timeout: 60_000 });
|
|
await expect.poll(async () => reloaded.getAttribute("data-camera-locked"), { timeout: 30_000 }).toBe("true");
|
|
});
|