Advance M8-M11 parity workflows
This commit is contained in:
76
web/tests/e2e/paint-stroke-session.spec.ts
Normal file
76
web/tests/e2e/paint-stroke-session.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user