import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const fixture = fs.readFileSync(path.resolve( import.meta.dirname, "../../../tests/files/web/nla_time_mapping_scene.blend", )); test("M10-12 moves one NLA strip through Main, undo, redo and save/reopen", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const result = await page.evaluate(async ({ bytes }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; const first = new WebEngineClient({ timeoutMs: 60_000 }); await first.init(); const opened = await first.openBlend(source); const track = opened.snapshot.nlaTracks?.find((candidate) => candidate.name === "M10 Time Mapping"); const strip = track?.strips.find((candidate) => candidate.id === "M10 Scaled Clip"); if (!track || !strip) throw new Error("NLA move fixture is incomplete"); const initialRevision = opened.snapshot.revision; const initialTracks = JSON.stringify(opened.snapshot.nlaTracks); let staleCode = ""; try { await first.applyCommand({ type: "moveNLAStrip", objectId: track.ownerId, trackId: track.id, stripId: strip.id, frameStart: 5, baseRevision: initialRevision - 1, }); } catch (error) { staleCode = (error as { code?: string }).code ?? ""; } const afterStale = await first.snapshot(); const moved = await first.applyCommand({ type: "moveNLAStrip", objectId: track.ownerId, trackId: track.id, stripId: strip.id, frameStart: 5, baseRevision: initialRevision, }); const movedStrip = moved.snapshot.nlaTracks?.find((candidate) => candidate.name === track.name) ?.strips.find((candidate) => candidate.id === strip.id); const undone = await first.applyCommand({ type: "undo" }); const undoneStrip = undone.snapshot.nlaTracks?.find((candidate) => candidate.name === track.name) ?.strips.find((candidate) => candidate.id === strip.id); const redone = await first.applyCommand({ type: "redo" }); const redoneStrip = redone.snapshot.nlaTracks?.find((candidate) => candidate.name === track.name) ?.strips.find((candidate) => candidate.id === strip.id); const saved = await first.saveBlend(); first.terminate(); const second = new WebEngineClient({ timeoutMs: 60_000 }); await second.init(); const reopened = await second.openBlend(saved); const reopenedStrip = reopened.snapshot.nlaTracks?.find((candidate) => candidate.name === track.name) ?.strips.find((candidate) => candidate.id === strip.id); await second.applyCommand({ type: "setFrame", frame: 10 }); const report = await second.evaluateDepsgraph(); const mesh = report.depsgraph.meshes.find((candidate) => candidate.objectId === track.ownerId); second.terminate(); return { staleCode, stalePreserved: JSON.stringify(afterStale.snapshot.nlaTracks) === initialTracks, revisions: [initialRevision, moved.snapshot.revision, undone.snapshot.revision, redone.snapshot.revision], delta: [moved.delta.baseRevision, moved.delta.nextRevision], moved: movedStrip ? [movedStrip.frameStart, movedStrip.frameEnd] : null, undone: undoneStrip ? [undoneStrip.frameStart, undoneStrip.frameEnd] : null, redone: redoneStrip ? [redoneStrip.frameStart, redoneStrip.frameEnd] : null, reopened: reopenedStrip ? [reopenedStrip.frameStart, reopenedStrip.frameEnd] : null, evaluatedX: mesh?.worldMatrix[3] ?? null, evaluationStatus: report.depsgraph.status, }; }, { bytes: new Uint8Array(fixture) }); expect(result.staleCode).toBe("REVISION_CONFLICT"); expect(result.stalePreserved).toBe(true); expect(result.revisions).toEqual([ result.revisions[0], result.revisions[0] + 1, result.revisions[0] + 2, result.revisions[0] + 3, ]); expect(result.delta).toEqual([result.revisions[0], result.revisions[0] + 1]); expect(result.moved).toEqual([5, 25]); expect(result.undone).toEqual([20, 40]); expect(result.redone).toEqual([5, 25]); expect(result.reopened).toEqual([5, 25]); expect(result.evaluationStatus).toBe("EVALUATED"); expect(result.evaluatedX).toBeCloseTo(2.5, 5); });