Files
Endoleap/tests/headless/package-browser-cases.mjs
T

56 lines
6.5 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.
/**
* 浏览器直接消费 /package 的编译 ESM:不依赖 TypeScript stripping 或应用 bundler。
* 真实测试 module Worker、PNG 解码、SHA-256 及浏览器存储恢复;网络显式阻止,不验证后端写入。
*/
import {createRegistry,builtinComponents,createDocument} from '/package/modules/canvas-core/index.js';
import {openProjectEngine} from '/package/modules/canvas-runtime/index.js';
import {createFoundryPersistence} from '/package/modules/canvas-service/index.js';
import {createBrowserStorage,createBrowserDigests,createBrowserImageDecoder} from '/package/modules/canvas-runtime/browser.js';
// 由浏览器检查入口调用并返回已通过场景的文字;所有断言在实际浏览器能力上执行。
export async function run(){
// 最小断言在页面端抛错,由外层 runner 读取失败;results 只有对应断言完成后才追加。
const assert=(condition,message)=>{if(!condition)throw Error(message);},results=[];
// 每次使用随机数据库名,创建生产 storage、digest 和 image 适配器,避免复用其他测试数据库。
const name=`headless-package-${crypto.randomUUID()}`,storage=createBrowserStorage({name}),digests=createBrowserDigests(),images=createBrowserImageDecoder();
// 独立随机项目和空文档基线;重开时仍使用这组 scope 和同一 storage。
const scope={accountId:crypto.randomUUID(),workspaceId:crypto.randomUUID(),projectId:crypto.randomUUID()},registry=createRegistry(builtinComponents()),document=createDocument(scope.projectId,[{id:scope.projectId,parent:null}],[]);
// 记录 Worker URL 和活跃数;保存原生构造器以便清理时恢复全局对象。
const workerUrls=[],NativeWorker=globalThis.Worker;let active=0;
// 包装真实 Worker 而非模拟消息:构造成功后计数,terminate 只扣减一次,仍调用原生终止方法。
globalThis.Worker=class extends NativeWorker{
// 先完成原生构造,再记录 URL 与活跃计数;保留正常 Worker 消息和错误行为。
constructor(url,...args){super(url,...args);workerUrls.push(String(url));active++;this.live=true;}
// 同一实例多次终止只扣减一次计数,避免负数掩盖泄漏;每次仍调用原生 terminate。
terminate(){if(this.live){this.live=false;active--;}super.terminate();}};
let engine;
try{
// 用 OffscreenCanvas 生成真实 4×2 PNG,检查编译图片 Worker 的尺寸结果。
const canvas=new OffscreenCanvas(4,2);canvas.getContext('2d').fillRect(0,0,4,2);const bytes=await canvas.convertToBlob({type:'image/png'});
const dimensions=await images.decode(bytes);assert(dimensions.width===4&&dimensions.height===2,'compiled image Worker dimensions');
// 将编译哈希 Worker 结果与浏览器原生 SHA-256 对照,不使用预设摘要跳过实际字节读取。
const digest=await digests.digest(bytes),expected=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await bytes.arrayBuffer()))].map(value=>value.toString(16).padStart(2,'0')).join('');assert(digest===expected,'compiled digest Worker SHA256');
// Worker 必须请求发布的 .js 文件,任务结束后活跃数为零;这验证 URL 布局和正常结束时的释放。
assert(workerUrls.some(url=>url.endsWith('/image.worker.js'))&&workerUrls.some(url=>url.endsWith('/digest.worker.js')),'Worker URLs did not resolve emitted JS');assert(active===0,'compiled Workers leaked');
results.push('compiled image and digest module Workers load emitted JS, decode a real PNG, match native SHA256 and terminate');
// 再验证 16 MiB 数据摘要及释放;耗时仅记为本机观察值,没有性能 SLA 断言。
const largeBytes=new Blob([new Uint8Array(16*1024*1024).fill(97)]),largeExpected=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await largeBytes.arrayBuffer()))].map(value=>value.toString(16).padStart(2,'0')).join(''),started=performance.now(),largeDigest=await digests.digest(largeBytes),digestMs=performance.now()-started;assert(largeDigest===largeExpected&&active===0,'large compiled digest returned wrong bytes or retained its Worker');results.push('16 MiB compiled digest Worker matched SHA256 in '+digestMs.toFixed(1)+' ms (observation, not an SLA)');
// 显式禁止网络派发,单独验证实际浏览器持久化适配器,避免引入虚构后端协议。
// 借用真实编译端口但声明不支持任何请求,因此编辑只保存在本地;若意外进入网络就直接失败。
const persistence={...createFoundryPersistence({registry,rootId:scope.projectId,workspaceId:scope.workspaceId,transport:async()=>{throw Error('unexpected network');}}),supportsRequest:()=>false};
// 以相同 scope 重新装配引擎,触发真实浏览器存储的 owner 申请和镜像恢复。
const open=()=>openProjectEngine({scope,registry,document,versions:new Map([[scope.projectId,1]]),storage,persistence,createId:()=>crypto.randomUUID()});
engine=await open();
// 创建一次文本元素和一个临时视图,前者及历史应恢复,后者必须随旧会话消失。
const target={id:crypto.randomUUID(),incarnation:1};
assert(engine.editor.dispatch({type:'element.create',element:{...target,ownerCanvasId:scope.projectId,componentId:'text',dataVersion:1,structure:'node',data:{title:'Packed recovery',text:'persisted'},geometry:{x:12,y:24,width:160,height:80,rotation:0}}}).status==='applied','compiled command rejected');
engine.sessions.attach('temporary-view');
// 等待本地保存后关闭/重开,检查修订不回退、文档和一个 undo 单元保留、views 为空。
await engine.settle();const revision=engine.getSnapshot().rootRevision;await engine.close();engine=await open();
assert(engine.getSnapshot().rootRevision>=revision,'compiled IndexedDB recovery revision regressed');assert(engine.getSnapshot().document.elements.get(target.id)?.data.text==='persisted','compiled durable document lost');assert(engine.getSnapshot().history.undo.length===1,'compiled durable history lost');assert(engine.sessions.getSnapshot().views.size===0,'transient sessions persisted');await engine.settle();
results.push('compiled browser storage and engine claim/release Web Locks and recover an edited document/history from IndexedDB without persisting views');
// 仅在全部 Worker 和存储断言通过后返回场景结果。
return results;
// 清理当前引擎和适配器,恢复原生 Worker,并发起删除测试数据库;这里不等待 deleteDatabase 的完成事件。
}finally{await engine?.close();images.close();digests.close();storage.close();globalThis.Worker=NativeWorker;indexedDB.deleteDatabase(name);}
}