import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend"); const largeBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/image_resource_matrix.blend"); test("M7-03 records exact streamed byte progress for a large valid blend", async ({ page }) => { test.setTimeout(60_000); const expectedBytes = fs.statSync(largeBlend).size; expect(expectedBytes).toBeGreaterThanOrEqual(512 * 1024); await page.goto("/"); await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 }); await page.getByTestId("blend-file-input").setInputFiles(largeBlend); const app = page.locator(".blender-app"); await expect(app).toHaveAttribute("data-user-action-open-status", "SUCCEEDED", { timeout: 30_000 }); await expect(app).toHaveAttribute("data-open-read-phase", "COMPLETED"); await expect(app).toHaveAttribute("data-open-read-bytes", String(expectedBytes)); await expect(app).toHaveAttribute("data-open-read-total", String(expectedBytes)); expect(Number(await app.getAttribute("data-open-read-events"))).toBeGreaterThan(2); expect(await app.getAttribute("data-open-read-action-id")).toBe(await app.getAttribute("data-user-action-open-id")); }); test("M7-03 cancels a large streamed open before WebEngine and preserves the current project", async ({ page }) => { test.setTimeout(60_000); await page.goto("/"); await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 }); await page.getByTestId("blend-file-input").setInputFiles(basicBlend); await expect(page.getByText("BasicCube", { exact: true })).toBeVisible(); const previousStats = await page.getByTestId("scene-stats").textContent(); const totalBytes = 40 * 1024 * 1024; const syntheticLargeBlend = Buffer.alloc(totalBytes, 0x7f); fs.readFileSync(basicBlend).copy(syntheticLargeBlend); await page.getByTestId("blend-file-input").setInputFiles({ name: "cancelled-large.blend", mimeType: "application/octet-stream", buffer: syntheticLargeBlend, }); const observation = await page.waitForFunction((expectedTotal) => { const element = document.querySelector('[data-testid="open-progress"]'); const bytesRead = Number(element?.getAttribute("data-bytes-read")); const actualTotal = Number(element?.getAttribute("data-total-bytes")); const cancel = document.querySelector('button[aria-label="取消打开"]'); if (bytesRead <= 0 || bytesRead >= expectedTotal || !cancel) return false; cancel.focus(); cancel.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true })); return { bytesRead, totalBytes: actualTotal }; }, totalBytes, { polling: "raf", timeout: 10_000 }); const observed = await observation.jsonValue() as { bytesRead: number; totalBytes: number }; expect(observed.bytesRead).toBeLessThan(totalBytes); expect(observed.totalBytes).toBe(totalBytes); const app = page.locator(".blender-app"); await expect(app).toHaveAttribute("data-user-action-open-status", "CANCELLED"); await expect(app).toHaveAttribute("data-user-action-open-error", "OPEN_CANCELLED"); await expect(app).toHaveAttribute("data-open-read-phase", "CANCELLED"); await expect(app).toHaveAttribute("data-open-cleanup-reader-count", "0"); await expect(app).toHaveAttribute("data-open-cleanup-reader-bytes", "0"); await expect(app).toHaveAttribute("data-open-cleanup-reader-staging", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-requests", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-bytes", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-handles", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-staging", "0"); const cancelledBytes = Number(await app.getAttribute("data-open-read-bytes")); expect(cancelledBytes).toBeGreaterThan(0); expect(cancelledBytes).toBeLessThan(totalBytes); await expect(page.getByTestId("scene-stats")).toHaveText(previousStats ?? ""); await expect(page.getByText("BasicCube", { exact: true })).toBeVisible(); expect(await page.evaluate(() => localStorage.getItem("blender-web:last-project-id"))).toBe("basic_scene"); }); test("M7-04 destroys isolated native open resources before reporting cancellation", async ({ page }) => { test.setTimeout(60_000); await page.goto("/"); await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 }); await page.getByTestId("blend-file-input").setInputFiles(basicBlend); await expect(page.getByText("BasicCube", { exact: true })).toBeVisible(); const previousStats = await page.getByTestId("scene-stats").textContent(); await page.getByTestId("blend-file-input").setInputFiles(largeBlend); const cancellation = await page.waitForFunction(() => { const progress = document.querySelector('[data-testid="open-progress"]'); const stage = progress?.dataset.stage; const cancel = document.querySelector('button[aria-label="取消打开"]'); if (!cancel || (stage !== "NATIVE_INITIALIZE" && stage !== "NATIVE_OPENED")) return false; cancel.click(); return stage; }, undefined, { polling: "raf", timeout: 10_000 }); expect(["NATIVE_INITIALIZE", "NATIVE_OPENED"]).toContain(await cancellation.jsonValue()); const app = page.locator(".blender-app"); await expect(app).toHaveAttribute("data-user-action-open-status", "CANCELLED"); await expect(app).toHaveAttribute("data-user-action-open-error", "OPEN_CANCELLED"); await expect(app).toHaveAttribute("data-open-cleanup-reader-count", "0"); await expect(app).toHaveAttribute("data-open-cleanup-reader-bytes", "0"); await expect(app).toHaveAttribute("data-open-cleanup-reader-staging", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-requests", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-bytes", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-handles", "0"); await expect(app).toHaveAttribute("data-open-cleanup-engine-staging", "0"); await expect(page.getByTestId("scene-stats")).toHaveText(previousStats ?? ""); await expect(page.getByText("BasicCube", { exact: true })).toBeVisible(); await page.getByRole("button", { name: "添加立方体" }).click(); await expect(page.getByTestId("scene-stats")).toContainText("Objects 4"); const download = page.waitForEvent("download"); await page.getByRole("button", { name: "保存项目" }).click(); await download; });