/** * 键盘输入路由的纯逻辑回归。通过上下文标记和简化closest/composedPath对象,测试输入优先级而不创建真实浏览器DOM。 * 回调计数用于确认被文本/IME/浮层认领的事件不会继续触发文档快捷键。 */ import {test} from 'node:test';import assert from 'node:assert/strict'; import {routeCanvasKey,canvasKeyContext} from '../../src/lib/canvas-bindings/input-router.ts'; // 逐个设置textEditing/composing/overlay/defaultPrevented,Backspace必须在动作层之前退出,gesture与shortcut调用数都为0。 test('text, IME, overlays and already consumed input never execute document shortcuts',()=>{for(const flags of [{textEditing:true},{composing:true},{overlay:true},{defaultPrevented:true}]){let calls=0;const result=routeCanvasKey({key:'Backspace',...flags},{gesture:()=>{calls++;return true;},shortcut:()=>{calls++;return true;}});assert.equal(calls,0);assert.ok(['text','overlay','unclaimed'].includes(result));}}); // 依次让gesture/tool/shortcut成为首个赢家,只运行到认领层;无处理器返回unclaimed。 test('gesture then tool then shortcut claim once; declining handlers fall through',()=>{for(const winner of ['gesture','tool','shortcut']){const calls=[];const handlers=Object.fromEntries(['gesture','tool','shortcut'].map(layer=>[layer,()=>{calls.push(layer);return layer===winner;}]));assert.equal(routeCanvasKey({key:'Escape'},handlers),winner);assert.deepEqual(calls,['gesture','tool','shortcut'].slice(0,['gesture','tool','shortcut'].indexOf(winner)+1));}assert.equal(routeCanvasKey({key:'x'},{}),'unclaimed');}); // 用伪closest模拟select和注册浮层,并传空/无效路径项,确认上下文识别不依赖全局Element。 test('DOM input context recognizes registered overlays and native selects without browser globals',()=>{ const select={closest:selector=>selector.split(',').includes('select')?select:null}; const overlay={closest:selector=>selector.includes('[data-canvas-overlay-view]')?overlay:null}; assert.deepEqual(canvasKeyContext({target:select}),{textEditing:true,overlay:false}); assert.deepEqual(canvasKeyContext({target:overlay}),{textEditing:false,overlay:true}); assert.deepEqual(canvasKeyContext({target:null,composedPath:()=>[null,{},42]}),{textEditing:false,overlay:false}); }); // 事件target已被Shadow DOM重定向到host,composedPath仍含textarea;Escape必须留给内部文本而非取消画布手势。 test('retargeted shadow events retain their inner text owner and never reach a gesture',()=>{ const input={closest:selector=>selector.split(',').includes('textarea')?input:null},host={closest:()=>null}; const context=canvasKeyContext({target:host,composedPath:()=>[input,host]});let canceled=false; assert.equal(routeCanvasKey({key:'Escape',...context},{gesture:()=>{canceled=true;return true;}}),'text');assert.equal(canceled,false); });