import type { DocumentSnapshot, EditorReconciliationRequest } from '@inkfinite/wasm';
import type { EditorDocument } from '@inkfinite/core';
import {
createEditorReconciliationRequest,
fromEditorProjection
} from '@inkfinite/core/persistence';
import { afterEach, describe, expect, it } from 'vitest';
import {
getSharedDocumentEngineWorker,
resetSharedDocumentEngineWorker
} from '../persistence/document-engine';
const snapshot: DocumentSnapshot = {
format: 'inkfinite.document',
format_version: 2,
document_id: 'document:browser-smoke',
heads: [],
document: {
pages: {
'page:one': { id: 'page:one', name: 'Page 1', layer_ids: ['layer:one'], version: 1 }
},
page_ids: ['page:one'],
layers: {
'layer:one': {
id: 'layer:one',
page_id: 'page:one',
name: 'Default',
shape_ids: [],
visible: true,
locked: false,
opacity: 1,
version: 1
}
},
shapes: {},
bindings: {},
assets: {}
}
};
const request: EditorReconciliationRequest = {
patches: [
{
type: 'create_shape',
shape: {
id: 'shape:rect',
kind: 'rect',
properties: { w: 20, h: 10, fill: '#ff0000' },
metadata: null,
style: { opacity: 1, fill_opacity: null, stroke_opacity: null },
layout: null
},
parent: { kind: 'layer', id: 'layer:one' },
transform: { a: 1, b: 0, c: 0, d: 1, e: 4, f: 5 },
anchor: { position: 'last' }
}
],
actor_id: 'browser',
origin: 'human',
transaction_id: 'transaction:create',
description: 'Create rectangle',
timestamp: 1
};
afterEach(resetSharedDocumentEngineWorker);
describe('compiled document engine worker', () => {
it('runs the canonical browser document lifecycle', async () => {
const worker = getSharedDocumentEngineWorker();
let state = await worker.createDocument(snapshot, 'browser');
expect(state.editor_projection.shapes).toEqual({});
state = await worker.applyEditorPatches(request);
expect(state.editor_projection.shapes['shape:rect']).toBeDefined();
expect(state.can_undo).toBe(true);
const projected = fromEditorProjection(state.editor_projection, state.snapshot);
const beforeEdit: EditorDocument = {
pages: projected.pages,
layers: projected.layers,
shapes: projected.shapes,
bindings: projected.bindings,
...(projected.assets ? { assets: projected.assets } : {})
};
const shape = beforeEdit.shapes['shape:rect'];
if (!shape) throw new Error('Expected the created rectangle in the editor projection');
const afterEdit: EditorDocument = {
...beforeEdit,
shapes: { ...beforeEdit.shapes, [shape.id]: { ...shape, x: 14, y: 15 } }
};
const moveRequest = createEditorReconciliationRequest(beforeEdit, afterEdit, {
actor_id: 'browser',
origin: 'human',
transaction_id: 'transaction:move',
description: 'Move rectangle',
timestamp: 2
});
state = await worker.applyEditorPatches(moveRequest);
expect(state.snapshot.document.shapes['shape:rect']?.transform.translation).toEqual({
x: 14,
y: 15
});
state = await worker.applyEditorPatches({
...request,
transaction_id: 'transaction:convert',
description: 'Convert rectangle',
patches: [
{
type: 'convert_shape',
shape_id: 'shape:rect',
kind: 'ellipse',
properties: { w: 20, h: 10, fill: '#ff0000' },
style: null
}
]
});
expect(state.snapshot.document.shapes['shape:rect']?.kind).toBe('ellipse');
expect(state.snapshot.document.shapes['shape:rect']?.style.opacity).toBe(1);
expect((await worker.undoDocument()).snapshot.document.shapes['shape:rect']?.kind).toBe(
'rect'
);
state = await worker.redoDocument();
expect(state.snapshot.document.shapes['shape:rect']).toBeDefined();
const reopened = await worker.openDocument(state.bytes, 'browser');
expect(reopened.snapshot.document.shapes['shape:rect']).toBeDefined();
expect(reopened.editor_projection.shapes['shape:rect']).toBeDefined();
const imported = await worker.importDocumentSvg(
new TextEncoder().encode(''),
'line.svg',
'page:one',
'layer:one',
2
);
expect(imported.shape_ids.length).toBeGreaterThan(1);
const importedChildId = imported.shape_ids.at(-1)!;
expect(imported.editor_projection.shapes[importedChildId]).toBeDefined();
const rendered = await worker.render(imported.snapshot, { page_id: 'page:one' });
expect(rendered.status).toBe('success');
if (rendered.status === 'success') {
expect(rendered.svg).toContain('