import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const attributeBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/attribute_scene.blend"); test("M9-10 commits pointer chunks as one Main revision and one undo step", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const blendBytes = fs.readFileSync(attributeBlend); const result = await page.evaluate(async ({ blendBytes }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const engine = new WebEngineClient({ timeoutMs: 60_000 }); await engine.init(); const opened = await engine.openBlend(blendBytes.buffer.slice(blendBytes.byteOffset, blendBytes.byteOffset + blendBytes.byteLength)); const mesh = opened.snapshot.meshes.find((candidate) => candidate.id === "mesh:AttributeMesh"); if (!mesh || mesh.vertexCount < 4) throw new Error("paint fixture is missing its vertex domain"); const pointerSessionId = "paint-pointer:chromium-main-1"; const session = { schemaVersion: 1 as const, pointerSessionId, baseRevision: opened.snapshot.revision, target: { mode: "VERTEX_COLOR" as const, meshId: mesh.id, attributeName: "M9StrokeColor", domain: "POINT" as const }, }; const started = await engine.beginPaintStroke(session); const first = await engine.appendPaintStrokeChunk({ schemaVersion: 1, pointerSessionId, baseRevision: session.baseRevision, chunkIndex: 0, indices: [0, 1], values: [1, 0, 0, 1, 0, 1, 0, 1] }); const during = await engine.snapshot(); const second = await engine.appendPaintStrokeChunk({ schemaVersion: 1, pointerSessionId, baseRevision: session.baseRevision, chunkIndex: 1, indices: [1, 2, 3], values: [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1] }); const committed = await engine.commitPaintStroke({ schemaVersion: 1, pointerSessionId, baseRevision: session.baseRevision, expectedChunkCount: 2 }); const committedMesh = committed.snapshot.meshes.find((candidate) => candidate.id === mesh.id); const undone = await engine.applyCommand({ type: "undo" }); const undoneMesh = undone.snapshot.meshes.find((candidate) => candidate.id === mesh.id); let secondUndoCode = ""; try { await engine.applyCommand({ type: "undo" }); } catch (error) { secondUndoCode = (error as { code?: string }).code ?? String(error); } const redone = await engine.applyCommand({ type: "redo" }); const redoneMesh = redone.snapshot.meshes.find((candidate) => candidate.id === mesh.id); const cancelId = "paint-pointer:chromium-cancel-2"; const cancelBase = redone.snapshot.revision; await engine.beginPaintStroke({ ...session, pointerSessionId: cancelId, baseRevision: cancelBase }); await engine.appendPaintStrokeChunk({ schemaVersion: 1, pointerSessionId: cancelId, baseRevision: cancelBase, chunkIndex: 0, indices: [0], values: [0, 0, 0, 1] }); const cancelled = await engine.cancelPaintStroke({ schemaVersion: 1, pointerSessionId: cancelId, baseRevision: cancelBase }); const afterCancel = await engine.snapshot(); engine.terminate(); const hasAttribute = (candidate: typeof mesh | undefined) => candidate?.attributes?.some((attribute) => attribute.name === "M9StrokeColor" && attribute.domain === "POINT") ?? false; return { started, first, second, duringRevision: during.snapshot.revision, committedRevision: committed.snapshot.revision, committedReceipt: committed.paintStrokeSession, committedAttribute: hasAttribute(committedMesh), undoneAttribute: hasAttribute(undoneMesh), redoneAttribute: hasAttribute(redoneMesh), secondUndoCode, cancelled, cancelBase, afterCancelRevision: afterCancel.snapshot.revision, }; }, { blendBytes: new Uint8Array(blendBytes) }); expect(result.started).toMatchObject({ state: "OPEN", chunkCount: 0 }); expect(result.first).toMatchObject({ state: "OPEN", chunkCount: 1, receivedEntryCount: 2, uniqueEntryCount: 2 }); expect(result.second).toMatchObject({ state: "OPEN", chunkCount: 2, receivedEntryCount: 5, uniqueEntryCount: 4 }); expect(result.duringRevision).toBe(result.started.baseRevision); expect(result.committedRevision).toBe(result.started.baseRevision + 1); expect(result.committedReceipt).toMatchObject({ state: "COMMITTED", chunkCount: 2, receivedEntryCount: 5, uniqueEntryCount: 4, committedRevision: result.committedRevision }); expect(result.committedAttribute).toBe(true); expect(result.undoneAttribute).toBe(false); expect(result.redoneAttribute).toBe(true); expect(result.secondUndoCode).toBe("INVALID_ARGUMENT"); expect(result.cancelled.state).toBe("CANCELLED"); expect(result.afterCancelRevision).toBe(result.cancelBase); });