docs(tests): explain size history traversal and recovery capture semantics
This commit is contained in:
@@ -22,6 +22,7 @@ function setup(options = {}) {
|
||||
return { editor, entry, propose() { const { document, history } = editor.getSnapshot(); return planCreationSize(document, history, registry, entry, target, { width: 260, height: 130 }); } };
|
||||
}
|
||||
/** 从plan当前状态逆序撤销到空,再按undo顺序重做,对照最终图片;只适用于不含额外初始元素的调用。 */
|
||||
// 直接应用历史 changes/invert 来检查候选前后值连续性;不走 editor.history,也不模拟其恢复对象时的 incarnation 分配。
|
||||
function traverse(plan) {
|
||||
let document = plan.document;
|
||||
for (const id of [...plan.history.undo].reverse()) document = applyChanges(document, invert(plan.history.entries.get(id).changes));
|
||||
@@ -82,7 +83,7 @@ test('an undone creation cannot be enriched', () => {
|
||||
// 删除再恢复使生命周期变化,即使ID相同,旧创建尺寸任务也不能修改新对象。
|
||||
test('a restored element has a new incarnation and rejects the old asynchronous task', () => {
|
||||
const { editor, propose } = setup(); editor.dispatch({ type: 'element.remove', target }); editor.history.undo();
|
||||
// Undo of removal creates a new incarnation: an old asynchronous task is stale.
|
||||
// 删除撤销会恢复为新 incarnation,原 target 仍绑定第一代,因此旧尺寸任务必须失效。
|
||||
assert.throws(propose, { code: 'creation_size_target_changed' });
|
||||
});
|
||||
// 用已有historyTransition格式描述plan,JSON往返后正反回放仍精确相等,entries不暴露set。
|
||||
@@ -138,6 +139,7 @@ test('a future resize followed by movement retains the user size through both re
|
||||
editor.dispatch({ type: 'element.geometry', target, patch: { height: 300 } });
|
||||
editor.dispatch({ type: 'element.geometry', target, patch: { x: 75 } });
|
||||
editor.history.undo(); editor.history.undo();
|
||||
// redo 栈顶在尾端,故正向重做倒序取项;回卷则按原数组顺序应用逆变化。
|
||||
const plan = propose(); let document = plan.document;
|
||||
for (const id of [...plan.history.redo].reverse()) document = applyChanges(document, plan.history.entries.get(id).changes);
|
||||
assert.equal(document.elements.get('image').geometry.height, 300);
|
||||
|
||||
@@ -61,7 +61,7 @@ test('cooperative sealing captures unknown values before yielding and retains ex
|
||||
assert.deepEqual(calls,['getter','left','right']);
|
||||
mutable.title='outside';current='outside';
|
||||
const image=await pending;
|
||||
// A second uncached table below exercises the work ceiling with a coarse clock.
|
||||
// 下方另造未预热的大表,用静止时钟隔离工作量上限;即使时间预算不增长也必须让出。
|
||||
assert.ok(yields>0);assert.deepEqual(image,expected);assert.equal(isSealedRecoveryImage(image),true);
|
||||
assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength);
|
||||
const beforeCoarse=yields;
|
||||
@@ -82,6 +82,7 @@ test('cooperative preparation evaluates every unsafe occurrence and rejects erro
|
||||
await assert.rejects(sealRecoveryImageAsync(17,1,1,dataArray(Array.from({length:1000},(_,i)=>i)),{now:()=>ticks++,frameBudgetMs:1,yieldTask:async()=>{throw Error('yield_failed');}}),/yield_failed/);
|
||||
// 异步深度检查使用内部 dataRecord 包装;省略的对象字段和变成 null 的数组项占用深度不同。
|
||||
const nested=(n,leaf)=>{for(let i=0;i<n;i++)leaf=dataRecord({child:leaf});return leaf;};
|
||||
// 预热 leaf 只允许复用编码内容,不能把它的高度从外层深度预算中抹去。
|
||||
const leaf=freeze({value:1});encodeRecoveryValue(leaf);
|
||||
await assert.rejects(sealRecoveryImageAsync(17,1,1,nested(64,leaf)),/invalid_recovery_payload/);
|
||||
await assert.rejects(sealRecoveryImageAsync(17,1,1,nested(64,dataArray([undefined]))),/invalid_recovery_payload/);
|
||||
@@ -126,7 +127,7 @@ test('repeated owned containers reuse facts assembled earlier in the same asynch
|
||||
assert.ok(yields>0);assert.equal(image.payload.left,image.payload.right);
|
||||
assert.equal(image.payload.left.length,1000);assert.equal(image.payload.left[999].index,999);
|
||||
assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength);
|
||||
// Unsafe descendants still need independent values for every occurrence.
|
||||
// 相同容器身份不代表后代 getter 安全;每次出现仍需独立捕获,递增值必须依次为 1、2。
|
||||
let n=0;const unsafe=dataRecord({getValue:Object.freeze({get value(){return ++n;}})});
|
||||
const captured=await sealRecoveryImageAsync(17,1,1,dataArray([unsafe,unsafe]));
|
||||
assert.deepEqual(captured.payload,[{getValue:{value:1}},{getValue:{value:2}}]);
|
||||
@@ -171,6 +172,7 @@ test('unknown callbacks invalidate the detached JSON environment without changin
|
||||
// left 先捕获,bridge getter 改写数组 toJSON,right 后捕获;同一 image 内顺序变化必须与同步 codec 一致。
|
||||
const make=async()=>dataRecord({left:await ownedRecord(),bridge:{get value(){Object.defineProperty(Array.prototype,'toJSON',{configurable:true,value(){return ['changed-by-callback'];}});return 'bridge';}},right:await ownedRecord()});
|
||||
try{
|
||||
// 同步参照也会改写原型,先 cleanup 再构造异步输入,保证两次捕获从相同环境开始。
|
||||
const expected=sealRecoveryImage(17,1,1,await make());cleanup();
|
||||
const payload=await make();let tick=0,yields=0;
|
||||
const pending=sealRecoveryImageAsync(17,1,1,payload,{now:()=>tick++,frameBudgetMs:1,yieldTask:async()=>{yields++;}});
|
||||
|
||||
Reference in New Issue
Block a user