156 lines
24 KiB
JavaScript
156 lines
24 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {randomUUID} from 'node:crypto';
|
|
import {createDocument,applyChanges,createRegistry,builtinComponents} from '../../src/lib/canvas-core/index.ts';
|
|
import {openProjectEngine} from '../../src/lib/canvas-runtime/engine.ts';
|
|
import {createFoundryPersistence} from '../../src/lib/canvas-service/foundry.ts';
|
|
import {decodeRecovery} from '../../src/lib/canvas-runtime/recovery.ts';
|
|
const registry=createRegistry(builtinComponents()),root='10000000-0000-4000-8000-000000000001',child='10000000-0000-4000-8000-000000000002';
|
|
const scope={accountId:'10000000-0000-4000-8000-000000000003',workspaceId:'10000000-0000-4000-8000-000000000004',projectId:root};
|
|
const node=(id,ownerCanvasId=root)=>({id,incarnation:1,ownerCanvasId,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:'original'},geometry:{x:0,y:0,width:100,height:80,rotation:0}});
|
|
const a=node('A'),b=node('B'),frame={...node('F'),componentId:'frame'},c=node('C',child),edge={id:'E',incarnation:1,ownerCanvasId:root,componentId:'connection',dataVersion:1,structure:'connection',data:{},source:{elementId:'A'},target:{elementId:'C'},path:null};
|
|
const canvases=[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'F'}}],prefix=createDocument(root,[canvases[0]],[a]),full=createDocument(root,canvases,[a,b,frame,c,edge]);
|
|
const versions=new Map([[root,1],[child,1]]),state={version:1,sourceToken:'captured-read',declaredCanvasIds:[root,child],receivedElements:1,totalElements:5},target={id:'A',incarnation:1};
|
|
function fixture(){
|
|
let image=null,leader,server=full;const serverVersions=new Map(versions),calls=[],receipts=new Map(),errors=[];
|
|
const storage={broken:false,saved:[],async claim(scope){assert.equal(leader,undefined);const held={scope,fence:1,active:true,async release(){leader=undefined;held.active=false;}};leader=held;return held;},async recover(){return structuredClone(image);},async commit(held,batch){assert.strictEqual(held,leader);if(storage.broken)throw Error('quota');image=structuredClone(batch.image);storage.saved.push(image);return{durableRootRevision:image.rootRevision};},async readFile(){},close(){},image:()=>structuredClone(image)};
|
|
const model=(participants=[...serverVersions.keys()],complete=true)=>({documentFormatVersion:1,projectId:root,complete,canvases:[...server.canvases.values()].filter(item=>participants.includes(item.id)),elements:[...server.elements.values()].filter(item=>participants.includes(item.ownerCanvasId)),tombstones:Object.fromEntries(server.tombstones),versions:Object.fromEntries(participants.map(id=>[id,serverVersions.get(id)])),removedCanvasIds:[]});
|
|
const transport=async input=>{
|
|
if(input.method==='GET')return input.path.endsWith('/model')?{status:200,body:{model:model()}}:{status:200,body:receipts.get(input.path.split('/').at(-1))};
|
|
calls.push(input);const body=JSON.parse(input.body),participants=Object.keys(body.expected_versions);for(const id of participants)assert.equal(body.expected_versions[id],serverVersions.get(id));
|
|
if(!receipts.has(input.requestId)){server=applyChanges(server,body.operations.flatMap(item=>item.changes));for(const id of participants)serverVersions.set(id,serverVersions.get(id)+1);receipts.set(input.requestId,{protocol_version:2,request_id:input.requestId,status:'applied',operation_ids:body.operations.map(item=>item.id),model:model(participants,false)});}
|
|
return{status:200,body:receipts.get(input.requestId)};
|
|
};
|
|
const persistence=createFoundryPersistence({workspaceId:scope.workspaceId,rootId:root,registry,transport});
|
|
return{storage,persistence,calls,errors,server:()=>server,advance(changes){server=applyChanges(server,changes);serverVersions.set(root,serverVersions.get(root)+1);},currentVersions:()=>new Map(serverVersions),open:extra=>openProjectEngine({scope,registry,document:prefix,versions,loading:state,storage,persistence,createId:randomUUID,onError:error=>errors.push(error),...extra})};
|
|
}
|
|
|
|
test('loaded fields edit and undo/redo durably while no request freezes; full source adds tail and sends each edit once',async()=>{
|
|
const f=fixture();let engine=await f.open();try{
|
|
assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}}).status,'applied');assert.equal(engine.editor.dispatch({type:'element.geometry',target,patch:{x:25}}).status,'applied');await engine.settle();
|
|
assert.equal(f.calls.length,0);assert.equal(engine.getSnapshot().runtime.ledger.requests.size,0);assert.equal(f.storage.image().formatVersion,7);decodeRecovery(f.storage.image(),registry);
|
|
assert.equal(engine.editor.history.undo().status,'applied');assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();const history=engine.getSnapshot().history;
|
|
await engine.close();engine=await f.open();await engine.settle();assert.equal(f.calls.length,0);assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'local');assert.equal(engine.getSnapshot().document.elements.get('A').geometry.x,25);
|
|
const next=createDocument(root,[canvases[0]],[a,b]);assert.equal(engine.loading.accept(next,versions,{...state,receivedElements:2}).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'local');assert.equal(engine.getSnapshot().document.elements.size,2);assert.deepEqual(engine.getSnapshot().history.undo,history.undo);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.loading.getState(),undefined);assert.equal(engine.getSnapshot().document.elements.size,5);assert.equal(f.server().elements.get('A').data.text,'local');assert.equal(f.server().elements.get('A').geometry.x,25);assert.ok(f.calls.length>0);assert.equal(engine.getSnapshot().history.undo.length,2);assert.ok([...engine.getSnapshot().runtime.ledger.operations.values()].every(record=>record.phase==='resolved'));assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('incomplete scopes block structure/resources/references and untrusted marker removal without changing the root',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
for(const command of [{type:'element.remove',target},{type:'element.create',element:node('new')},{type:'element.move',target,canvasId:child},{type:'element.data',target,patch:{assetId:'asset'}},{type:'element.data',target,patch:{presentation:{scale:2}}}]){const before=engine.getSnapshot();assert.equal(engine.editor.dispatch(command).code,'model_loading');assert.strictEqual(engine.getSnapshot(),before);}
|
|
const before=engine.getSnapshot();assert.equal(engine.editor.updateRuntime(previous=>({...previous,loading:undefined})).code,'loading_state_owned');assert.strictEqual(engine.getSnapshot(),before);
|
|
assert.throws(()=>engine.stageFiles([{name:'x',bytes:new Blob(['x'])}],()=>{}),{code:'model_loading'});await assert.rejects(engine.imports.duplicate([target]),{code:'model_loading'});assert.equal(f.calls.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('full baseline must be durable before the hold releases; quota retry never sends from the partial cut',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
engine.editor.dispatch({type:'element.data',target,patch:{title:'saved edit'}});await engine.settle();f.storage.broken=true;
|
|
await assert.rejects(engine.loading.complete(full,versions,{sourceToken:state.sourceToken}),/quota/);assert.equal(f.calls.length,0);assert.equal(f.storage.image().formatVersion,7);assert.ok(engine.getSnapshot().runtime.ledger.requests.size===0);
|
|
f.storage.broken=false;await engine.retry();await engine.settle();assert.equal(f.server().elements.get('A').data.title,'saved edit');assert.ok(f.calls.length>0);assert.notEqual(f.storage.image().formatVersion,7);
|
|
}finally{f.storage.broken=false;await engine.close();}
|
|
});
|
|
test('late or changed prefix is rejected; a fresh full read preserves local edits and same-version new records',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}});await engine.settle();const before=engine.getSnapshot();
|
|
assert.equal(engine.loading.accept(prefix,versions,{...state,sourceToken:'another'}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
const changed=createDocument(root,[canvases[0]],[{...a,data:{...a.data,title:'remote'}}]);assert.equal(engine.loading.accept(changed,versions,state).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
assert.equal((await engine.loading.complete(full,versions,{fresh:true})).status,'applied');await engine.settle();const done=engine.getSnapshot();assert.equal(engine.loading.accept(prefix,versions,state).code,'loading_not_active');assert.strictEqual(engine.getSnapshot(),done);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v7 cannot lose its marker or downgrade, and recovery retains partial edit/history without requests',async()=>{
|
|
const f=fixture(),engine=await f.open();try{engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}});await engine.settle();const image=f.storage.image();assert.equal(decodeRecovery(image,registry).runtime.loading.sourceToken,state.sourceToken);
|
|
for(const formatVersion of [1,2,3,4,5,6])assert.throws(()=>decodeRecovery({...image,formatVersion},registry),{code:'unsupported_recovery_version'});
|
|
const missing=structuredClone(image);delete missing.payload.runtime.loading;assert.throws(()=>decodeRecovery(missing,registry),{code:'invalid_loading_state'});
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('a fresh conflicting completion keeps the original partial edit and resolves through the existing reviewed recovery path',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
const edit=engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}});await engine.settle();
|
|
f.advance([{kind:'field',target,field:'data.text',before:'original',after:'remote',beforePresent:true,afterPresent:true}]);
|
|
assert.equal((await engine.loading.complete(f.server(),f.currentVersions(),{fresh:true})).status,'rejected');assert.ok(engine.loading.getState());assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'local');assert.equal(f.calls.length,0);
|
|
const session=await engine.prepareRecovery();assert.ok(session.conflict);const plan=session.plan(new Map([[edit.operationIds[0],'keep-local']]));assert.equal((await session.resolve(plan)).status,'applied');await engine.settle();assert.equal(engine.loading.getState(),undefined);assert.equal(f.server().elements.get('A').data.text,'local');assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('reopening a partial image with a complete fresh model finishes through refresh without replacing history',async()=>{
|
|
const f=fixture();let engine=await f.open();try{engine.editor.dispatch({type:'element.data',target,patch:{title:'retained'}});await engine.settle();const history=engine.getSnapshot().history;await engine.close();engine=await f.open({document:full,loading:undefined});assert.ok(engine.loading.getState());assert.equal(engine.refresh(full,versions).status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').data.title,'retained');assert.deepEqual(engine.getSnapshot().history.undo,history.undo);}finally{await engine.close();}
|
|
});
|
|
|
|
test('permitted command cannot hide a reference write inside a feature handler',async()=>{
|
|
const {defineComponent,defineFeature}=await import('../../src/lib/canvas-core/index.ts');
|
|
const custom=defineComponent({id:'unsafe-handler',dataVersion:1,structure:'node',validate:()=>true,resourceFields:[],copy:{references:[{path:['link'],target:'element',external:'preserve'}],omit:[]},features:[defineFeature('side-effect',{'content.edit':({element})=>[{kind:'field',target:{id:element.id,incarnation:element.incarnation},field:'data.link',before:'old',after:'new',beforePresent:true,afterPresent:true}]})]});
|
|
const customRegistry=createRegistry([...builtinComponents(),custom]),record={...a,componentId:custom.id,data:{title:'A',link:'old'}},document=createDocument(root,[canvases[0]],[record]),f=fixture(),engine=await f.open({registry:customRegistry,document});
|
|
try{const before=engine.getSnapshot();assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{title:'allowed title'}}).code,'model_loading');assert.strictEqual(engine.getSnapshot(),before);assert.equal(f.calls.length,0);}finally{await engine.close();}
|
|
});
|
|
test('future loading interpreters, changed capture counts and forged frozen requests fail closed on recovery',async()=>{
|
|
const f=fixture(),engine=await f.open();try{await engine.persist();const source=f.storage.image();
|
|
for(const mutate of [image=>image.payload.runtime.loading.version=2,image=>image.payload.runtime.loading.receivedElements=99,image=>image.payload.runtime.loading.declaredCanvasIds=[],image=>image.payload.runtime.ledger.requests.push(['fake',{}])]){const image=structuredClone(source);mutate(image);assert.throws(()=>decodeRecovery(image,registry));}
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('fresh full read merges unrelated remote fields and geometry undo does not roll them back',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
engine.editor.dispatch({type:'element.geometry',target,patch:{x:25}});await engine.settle();f.advance([{kind:'field',target,field:'data.title',before:'A',after:'remote title',beforePresent:true,afterPresent:true}]);
|
|
assert.equal((await engine.loading.complete(f.server(),f.currentVersions(),{fresh:true})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get('A').data.title,'remote title');assert.equal(f.server().elements.get('A').geometry.x,25);
|
|
assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').geometry.x,0);assert.equal(f.server().elements.get('A').data.title,'remote title');
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('identity fencing rejects edits, history traversal and late prefix publication without changing the root',async()=>{
|
|
const f=fixture();let current=true;const engine=await f.open({isCurrentIdentity:()=>current});try{engine.editor.dispatch({type:'element.data',target,patch:{text:'saved'}});await engine.settle();current=false;const before=engine.getSnapshot();assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{text:'late'}}).code,'identity_changed');assert.equal(engine.editor.history.undo().code,'identity_changed');assert.equal(engine.loading.accept(prefix,versions,state).code,'identity_changed');assert.equal(engine.refresh(full,versions).code,'identity_changed');assert.strictEqual(engine.getSnapshot(),before);assert.equal(f.calls.length,0);}finally{await engine.close();}
|
|
});
|
|
|
|
test('background full reads are shared, preserve edits and cannot downgrade after completion',async()=>{
|
|
let release;const gate=new Promise(resolve=>release=resolve);const f=fixture();let reads=0;const persistence={...f.persistence,async read(){reads++;await gate;return{document:f.server(),versions:f.currentVersions()};}},engine=await f.open({persistence});
|
|
try{engine.editor.dispatch({type:'element.data',target,patch:{text:'background edit'}});await engine.persist();const first=engine.loading.resume(),second=engine.loading.resume();assert.strictEqual(first,second);while(!reads)await new Promise(resolve=>setImmediate(resolve));assert.equal(reads,1);release();assert.equal((await first).status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').data.text,'background edit');assert.equal(engine.loading.accept(prefix,versions,state).code,'loading_not_active');assert.equal((await engine.loading.resume()).status,'applied');assert.equal(reads,1);}finally{release();await engine.close();}
|
|
});
|
|
test('project close aborts a background read and persists its partial edits for the next owner',async()=>{
|
|
const f=fixture();let aborted=false,started;const reading=new Promise(resolve=>started=resolve);const persistence={...f.persistence,read:signal=>new Promise((resolve,reject)=>{started();signal.addEventListener('abort',()=>{aborted=true;reject(signal.reason);},{once:true});})};let engine=await f.open({persistence});
|
|
engine.editor.dispatch({type:'element.data',target,patch:{text:'survives source close'}});await engine.persist();const rejected=assert.rejects(engine.loading.resume());await reading;await engine.close();await rejected;assert.equal(aborted,true);assert.equal(f.storage.image().formatVersion,7);engine=await f.open();try{assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'survives source close');assert.equal(f.calls.length,0);}finally{await engine.close();}
|
|
});
|
|
test('Workspace opens one partial owner and ignores an older foreground result after background synchronization',async()=>{
|
|
const {createWorkspaceRuntime}=await import('../../src/lib/canvas-runtime/workspace.ts'),f=fixture();const workspace=createWorkspaceRuntime({accountId:scope.accountId,workspaceId:scope.workspaceId,authEpoch:1,registry,storage:f.storage,isCurrentIdentity:()=>true,adapters:()=>({persistence:f.persistence})});
|
|
try{const [first,second]=await Promise.all([workspace.openPartial(prefix,versions,state),workspace.openPartial(prefix,versions,state)]);assert.strictEqual(first,second);first.editor.dispatch({type:'element.data',target,patch:{title:'kept background'}});await first.loading.resume();await first.settle();const before=first.getSnapshot();assert.strictEqual(await workspace.open(full,versions),first);assert.strictEqual(first.getSnapshot(),before);assert.equal(first.getSnapshot().runtime.conflicts.size,0);assert.equal(workspace.getProjects().size,1);}finally{await workspace.close();}
|
|
});
|
|
|
|
test('Workspace reports a background source failure while preserving the pending partial cut and fences retired reports',async()=>{
|
|
const {createWorkspaceRuntime}=await import('../../src/lib/canvas-runtime/workspace.ts'),f=fixture(),reports=[];let current=true;const workspace=createWorkspaceRuntime({accountId:scope.accountId,workspaceId:scope.workspaceId,authEpoch:1,registry,storage:f.storage,isCurrentIdentity:()=>current,onError:error=>reports.push(error),adapters:()=>({persistence:{...f.persistence,async read(){throw Error('offline');}}})});
|
|
try{const engine=await workspace.openPartial(prefix,versions,state);engine.editor.dispatch({type:'element.data',target,patch:{text:'held while offline'}});await engine.persist();await assert.rejects(workspace.resumeLoading(root),/offline/);assert.equal(reports.length,1);assert.ok(engine.loading.getState());assert.equal(f.calls.length,0);assert.equal(f.storage.image().formatVersion,7);current=false;await assert.rejects(workspace.resumeLoading(root),/identity_changed/);assert.equal(reports.length,1);}finally{await workspace.close();}
|
|
});
|
|
|
|
test('certified scopes distinguish a complete child from an incomplete parent and publish coverage atomically',async()=>{
|
|
const loading={...state,version:2,canvasElementCounts:{[root]:4,[child]:1}},f=fixture();let engine=await f.open({loading});
|
|
try{
|
|
const first=engine.editor.getQueries();assert.equal(first.lookupElement('B').status,'unloaded');assert.equal(first.lookupElement('A').status,'present');assert.equal(first.membersResult(root).complete,false);assert.equal(first.hitNodes(root,{x:50,y:40}).complete,false);
|
|
const next=createDocument(root,canvases,[a,frame,c]);assert.equal(engine.loading.accept(next,versions,{...loading,receivedElements:3}).status,'applied');
|
|
const queries=engine.editor.getQueries();assert.equal(queries.membersResult(child).complete,true);assert.deepEqual(queries.membersResult(child).ids,['C']);assert.equal(queries.membersResult(root).complete,false);assert.equal(queries.childCanvasesResult(root).complete,true);assert.equal(queries.incidentResult('C').complete,false);assert.equal(queries.relations.referenceComplete,false);assert.equal(queries.coverage.projectComplete,false);assert.equal(first.lookupElement('C').status,'unloaded');
|
|
engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'child edit'}});await engine.persist();assert.equal(f.storage.image().formatVersion,8);assert.equal(f.calls.length,0);decodeRecovery(f.storage.image(),registry);
|
|
const source=f.storage.image();for(const formatVersion of [1,2,3,4,5,6,7])assert.throws(()=>decodeRecovery({...source,formatVersion},registry),{code:'unsupported_recovery_version'});
|
|
for(const mutate of [image=>delete image.payload.runtime.loading.canvasElementCounts,image=>image.payload.runtime.loading.version=1,image=>image.payload.runtime.loading.version=99,image=>image.payload.runtime.loading.canvasElementCounts[child]=0]){const image=structuredClone(source);mutate(image);assert.throws(()=>decodeRecovery(image,registry));}
|
|
await engine.close();engine=await f.open({loading});assert.equal(engine.editor.getQueries().membersResult(child).complete,true);assert.equal(engine.getSnapshot().document.elements.get('C').data.text,'child edit');
|
|
assert.equal(engine.loading.accept(full,versions,{...loading,receivedElements:5}).status,'applied');const before=engine.getSnapshot(),oldQueries=engine.editor.getQueries();assert.equal(oldQueries.lookupElement('missing').status,'unloaded');assert.equal(oldQueries.membersResult(root).complete,true);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:loading.sourceToken})).status,'applied');assert.equal(engine.getSnapshot().revision,before.revision,'coverage-only completion must not invent a document revision');assert.equal(engine.editor.getQueries().lookupElement('missing').status,'absent');assert.equal(engine.editor.getQueries().coverage.projectComplete,true);assert.equal(oldQueries.lookupElement('missing').status,'unloaded');await engine.settle();assert.equal(f.server().elements.get('C').data.text,'child edit');assert.equal(f.storage.image().formatVersion<7,true);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('legacy prefixes do not infer scope completion; a changed count manifest cannot replace the published root',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
assert.equal(engine.loading.accept(full,versions,{...state,receivedElements:5}).status,'applied');assert.equal(engine.editor.getQueries().membersResult(child).complete,false);
|
|
const before=engine.getSnapshot();assert.equal(engine.loading.accept(full,versions,{...state,version:2,canvasElementCounts:{[root]:4,[child]:1},receivedElements:5}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('zero-member scopes are complete only once their entry dependencies publish',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame]),loading={...state,version:2,totalElements:2,receivedElements:2,canvasElementCounts:{[root]:2,[child]:0}},engine=await f.open({document,loading});
|
|
try{assert.deepEqual(engine.editor.getQueries().membersResult(child),{ids:[],complete:true});assert.equal(engine.editor.getQueries().lookupElement('not-here').status,'unloaded');}finally{await engine.close();}
|
|
});
|
|
|
|
test('v2 admits path edits in a certified connection owner while another scope is incomplete, preserving undo and v8 recovery',async()=>{
|
|
const f=fixture(),loading={...state,version:2,receivedElements:5,totalElements:6,canvasElementCounts:{[root]:4,[child]:2}},engine=await f.open({document:full,loading}),path={type:'bezier-chain',knots:[{id:'k',mode:'corner',position:{x:50,y:25},in:{x:0,y:0},out:{x:0,y:0}}]};
|
|
try{
|
|
assert.equal(engine.editor.getQueries().membersResult(root).complete,true);assert.equal(engine.editor.getQueries().membersResult(child).complete,false);
|
|
const command={type:'connection.path',target:{id:'E',incarnation:1},path};assert.equal(engine.editor.can(command).enabled,true);assert.equal(engine.editor.dispatch(command).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('E').path.knots[0].position.x,50);
|
|
assert.equal(engine.editor.history.undo().status,'applied');assert.equal(engine.getSnapshot().document.elements.get('E').path,null);assert.equal(engine.editor.history.redo().status,'applied');await engine.persist();assert.equal(f.calls.length,0);assert.equal(f.storage.image().formatVersion,8);assert.equal(decodeRecovery(f.storage.image(),registry).document.elements.get('E').path.knots[0].position.x,50);
|
|
const before=engine.getSnapshot();assert.equal(engine.loading.accept(full,versions,{...loading,canvasElementCounts:{[root]:5,[child]:1}}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
}finally{await engine.close();}
|
|
const other=fixture(),incomplete=await other.open({document:full,loading:{...loading,canvasElementCounts:{[root]:5,[child]:1}}});try{assert.equal(incomplete.editor.can({type:'connection.path',target:{id:'E',incarnation:1},path}).enabled,false);}finally{await incomplete.close();}
|
|
});
|