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"); const TEXTURE = path.resolve(HERE, "../../app/public/tutorial/texture-paint/re-texture-base.png"); 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 clickWorkspace(page: import("@playwright/test").Page, name: string): Promise { await page.locator(".workspace-tabs button", { hasText: name }).evaluate((element) => (element as HTMLElement).click()); } async function assignReMaterialAndLocalTexture(page: import("@playwright/test").Page): Promise { await clickWorkspace(page, "Shading"); await page.locator(".pbr-asset-library").waitFor({ timeout: 60_000 }); await page.locator('select[aria-label="资产来源"]').selectOption("library:re-pbr-v1"); await expect.poll(async () => page.locator('[data-library="library:re-pbr-v1"]').count(), { timeout: 60_000 }).toBe(6); await page.locator('[data-library="library:re-pbr-v1"]', { hasText: "black_tiles_wall" }).first().dblclick(); await expect.poll(async () => page.locator('select[aria-label^="材质槽"]').first().inputValue(), { timeout: 120_000 }) .toContain("material:black_tiles_wall"); await page.locator('.property-section input[type="file"][accept="image/png,image/jpeg"]').first().setInputFiles(TEXTURE); await expect.poll(async () => (await page.locator('select[aria-label="材质图片"] option').allTextContents()).some((text) => text.includes("re-texture-base")), { timeout: 60_000 }) .toBe(true); await page.locator('select[aria-label="材质图片"]').first().selectOption({ label: "re-texture-base" }); await page.waitForTimeout(2_000); } async function openTexturePaint(page: import("@playwright/test").Page): Promise { await clickWorkspace(page, "Texture Paint"); await page.locator(".texture-paint-view").waitFor({ timeout: 60_000 }); await page.locator('[data-testid="texture-paint-canvas"]').waitFor({ timeout: 60_000 }); await expect.poll(async () => page.locator(".texture-paint-view").getAttribute("data-image-id"), { timeout: 60_000 }) .toContain("re-texture-base"); await expect.poll(async () => page.locator(".texture-paint-controls output").first().innerText(), { timeout: 60_000 }) .toContain("已加载"); } async function centerPixel(page: import("@playwright/test").Page): Promise<[number, number, number, number] | null> { return page.evaluate(() => { const canvas = document.querySelector('[data-testid="texture-paint-canvas"]'); const context = canvas?.getContext("2d"); if (!canvas || !context) return null; const data = context.getImageData(Math.floor(canvas.width * 0.5), Math.floor(canvas.height * 0.5), 1, 1).data; return [data[0], data[1], data[2], data[3]]; }); } async function paintRedThroughCenter(page: import("@playwright/test").Page): Promise { await page.locator('[aria-label="画笔颜色"]').fill("#ff0000"); await page.locator('[aria-label="画笔大小"]').fill("120"); const box = await page.locator('[data-testid="texture-paint-canvas"]').boundingBox(); if (!box) throw new Error("texture paint canvas has no box"); await page.mouse.move(box.x + box.width * 0.3, box.y + box.height * 0.5); await page.mouse.down(); await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.5, { steps: 10 }); await page.mouse.up(); await expect.poll(async () => page.locator(".texture-paint-view").getAttribute("data-last-action"), { timeout: 60_000 }) .toBe("笔画已提交"); } test("Texture Paint paints a local image, commits one stroke and undoes it", async ({ page }) => { test.setTimeout(300_000); await openStudioAndMonkey(page); await assignReMaterialAndLocalTexture(page); await openTexturePaint(page); const before = await centerPixel(page); await paintRedThroughCenter(page); const after = await centerPixel(page); expect(after?.[0]).toBeGreaterThan(200); expect(after?.[1]).toBeLessThan(40); expect(after?.[2]).toBeLessThan(40); await page.getByRole("button", { name: "撤销笔画", exact: true }).evaluate((element) => (element as HTMLElement).click()); await expect.poll(async () => page.locator(".texture-paint-view").getAttribute("data-last-action"), { timeout: 60_000 }).toBe("已撤销"); const undone = await centerPixel(page); expect(undone?.[0]).toBe(before?.[0]); expect(undone?.[1]).toBe(before?.[1]); expect(undone?.[2]).toBe(before?.[2]); }); test("Texture Paint makes a linked library image local before painting", async ({ page }) => { test.setTimeout(300_000); await openStudioAndMonkey(page); await clickWorkspace(page, "Shading"); await page.locator(".pbr-asset-library").waitFor({ timeout: 60_000 }); await page.locator('select[aria-label="资产来源"]').selectOption("library:re-pbr-v1"); await expect.poll(async () => page.locator('[data-library="library:re-pbr-v1"]').count(), { timeout: 60_000 }).toBe(6); await page.locator('[data-library="library:re-pbr-v1"]', { hasText: "black_tiles_wall" }).first().dblclick(); await expect.poll(async () => page.locator('select[aria-label^="材质槽"]').first().inputValue(), { timeout: 120_000 }).toContain("material:black_tiles_wall"); await clickWorkspace(page, "Texture Paint"); const view = page.locator(".texture-paint-view"); await view.waitFor({ timeout: 60_000 }); const imageSelect = view.getByLabel("纹理图像"); await expect.poll(async () => (await imageSelect.locator("option").allTextContents()).some((text) => text.includes("black_tiles_wall")), { timeout: 60_000 }).toBe(true); await imageSelect.selectOption({ label: (await imageSelect.locator("option").allTextContents()).find((text) => text.includes("black_tiles_wall"))! }); await view.getByRole("button", { name: "设为本地图像", exact: true }).evaluate((element) => (element as HTMLElement).click()); await expect.poll(async () => view.locator(".texture-paint-last-action").innerText(), { timeout: 60_000 }).toContain("已设为本地"); const canvas = view.getByTestId("texture-paint-canvas"); await canvas.waitFor({ timeout: 60_000 }); const box = await canvas.boundingBox(); if (!box) throw new Error("texture paint canvas has no box"); await page.mouse.move(box.x + box.width * 0.4, box.y + box.height * 0.5); await page.mouse.down(); await page.mouse.move(box.x + box.width * 0.6, box.y + box.height * 0.55, { steps: 12 }); await page.mouse.up(); await expect.poll(async () => view.getAttribute("data-last-action"), { timeout: 60_000 }).toBe("笔画已提交"); }); test("Texture Paint stroke survives an OPFS save and reopen", async ({ page }) => { test.setTimeout(360_000); await openStudioAndMonkey(page); await assignReMaterialAndLocalTexture(page); await openTexturePaint(page); await paintRedThroughCenter(page); 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 openTexturePaint(page); const pixel = await centerPixel(page); expect(pixel?.[0]).toBeGreaterThan(200); expect(pixel?.[1]).toBeLessThan(40); expect(pixel?.[2]).toBeLessThan(40); });