Files
Endoleap/tests/headless/import-document.test.mjs
T

25 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 完整文档 envelope 的索引导入源测试(parser 4)。头部/拓扑先验证,元素正文按稳定序号读取,原根 canvas 映射到目标根。
* 超大单条作为记录失败,不阻止后续合法项;文件尾结构损坏则拒绝打开完整索引源。
* parser 4 先扫描建立索引再开放 read;先出现 canvas 记录是输出顺序,不证明它在扫描尾部前已返回。
*/
import {test} from 'node:test';import assert from 'node:assert/strict';import {openDocumentImportSource} from '../../src/lib/canvas-runtime/import-document.ts';
// 独立信号和 Blob 包装工厂;基线包含两层 canvas、Unicode/分隔符正文及墓碑,检查封装而非简单数组。
const signal=()=>new AbortController().signal,source=value=>new Blob([typeof value==='string'?value:JSON.stringify(value)]),document={documentFormatVersion:1,projectId:'source',canvases:[{id:'root',parent:null},{id:'child',parent:{canvasId:'root',entryElementId:'frame'}}],elements:[{id:'frame',data:{title:'中文 [,] "'}},{id:'inside',ownerCanvasId:'child'}],tombstones:{old:2}};
// 为每次打开绑定固定 fingerprint;重开序号读取应得到同样记录,不依赖上次内存游标。
const open=bytes=>openDocumentImportSource({bytes,fingerprint:'original-file',signal:signal()});
// 子 canvas 先输出,原根只作 alias;frame/member 以稳定序号读取,重开后同序号结果一致,到末尾返回 null。
test('export envelope retains nested canvas records, emits root alias and seeks payloads by stable ordinal',async()=>{const reader=await open(source(document));assert.equal(reader.parserVersion,4);const a=await reader.read(0,signal()),b=await reader.read(1,signal());assert.equal(a.value.kind,'canvas');assert.equal(a.value.record.id,'child');assert.equal(a.value.sourceRootId,'root');assert.deepEqual(b.value.record,document.elements[0]);const reopened=await open(source(document));assert.deepEqual(await reopened.read(2,signal()),await reader.read(2,signal()));assert.equal(await reader.read(3,signal()),null);});
// envelope 字段顺序可变,但未来版本、多根、缺失父 canvas、非法墓碑及重复键都不能生成可用源。
test('envelope fields may be reordered, while future versions, duplicate keys and broken topology reject',async()=>{const reordered={elements:document.elements,tombstones:{},canvases:document.canvases,projectId:'source',documentFormatVersion:1};assert.equal((await(await open(source(reordered))).read(1,signal())).value.record.id,'frame');for(const bad of [{...document,documentFormatVersion:2},{...document,canvases:[...document.canvases,{id:'other',parent:null}]},{...document,canvases:[{id:'root',parent:null},{id:'child',parent:{canvasId:'missing',entryElementId:'e'}}]},{...document,tombstones:{x:0}},'{"documentFormatVersion":1,"documentFormatVersion":1}'])await assert.rejects(open(source(bad)));});
// 超过记录上限时不解码巨型正文并报告 record_too_large,后续记录仍可读;尾部垃圾及预先取消明确拒绝。
test('payload errors are per record and large records are never decoded; malformed envelope tails reject',async()=>{const reader=await open(source({...document,elements:[{id:'huge',text:'x'.repeat(300000)},document.elements[1]]}));assert.equal((await reader.read(1,signal())).error,'record_too_large');assert.equal((await reader.read(2,signal())).value.record.id,'inside');await assert.rejects(open(source(JSON.stringify(document)+'garbage')));const controller=new AbortController();controller.abort();await assert.rejects(openDocumentImportSource({bytes:source(document),fingerprint:'x',signal:controller.signal}),/import_aborted/);});
import {prepareNativeImport} from '../../src/lib/canvas-runtime/import-native.ts';
import {createImportJob} from '../../src/lib/canvas-runtime/imports.ts';
import {createRegistry,createDocument} from '../../src/lib/canvas-core/index.ts';
// 原根 alias 同时映射 owner 与组件声明的 canvasRef;不能为原根再 reserve 一个多余的新 canvas ID。
test('document root aliases also remap component-declared canvas references without reserving a duplicate root',()=>{const registry=createRegistry([{id:'ref',dataVersion:1,structure:'node',features:[],validate:()=>true,copy:{references:[{path:['canvasRef'],target:'canvas',external:'reject'}],omit:[]}}]),target=createDocument('p',[{id:'target',parent:null}],[]),job=createImportJob({id:'job',projectId:'p',parserVersion:4,sourceFingerprint:'file',source:{kind:'native-document',fileId:'file',byteLength:100,targetCanvasId:'target'}}),reserved=[],record={id:'a',incarnation:1,ownerCanvasId:'source',componentId:'ref',dataVersion:1,structure:'node',geometry:{x:0,y:0,width:10,height:10,rotation:0},data:{canvasRef:'source'}},unit=prepareNativeImport({cursor:0,nextCursor:1,key:'record:0',value:{kind:'element',key:'a',record,sourceRootId:'source'}},job,key=>{reserved.push(key);return'new-'+key;},target,registry);assert.equal(unit.changes[0].after.data.canvasRef,'target');assert.equal(unit.changes[0].after.ownerCanvasId,'target');assert.ok(!reserved.includes('canvas:source'));});
// 小于固定字节上限的扫描也要按耗时让出,yield 内取消后以 import_aborted 拒绝。
// now 每次读取人工增加 10,避免依赖机器快慢;只检查预算分支,不测实际主线程帧耗时。
test('document preparation yields by elapsed time even for files below the byte ceiling',async()=>{const controller=new AbortController();let clock=0,yields=0;await assert.rejects(openDocumentImportSource({bytes:source({...document,elements:[{id:'slow',text:'x'.repeat(2048)}]}),fingerprint:'slow',signal:controller.signal,now:()=>clock+=10,frameBudgetMs:6,yieldTask:async()=>{yields++;controller.abort();}}),/import_aborted/);assert.equal(yields,1);});