import assert from 'node:assert/strict'; import { test } from 'node:test'; import { loadTypescript } from './support/load-typescript.mjs'; const { initImagePaste } = await loadTypescript(new URL('../src/input/ImagePaste.ts', import.meta.url)); function harness() { const handlers = new Map(), accepted=[]; let enabled=true; const root={activeElement:null,addEventListener(type,cb,{signal}){handlers.set(type,cb);signal.addEventListener('abort',()=>handlers.delete(type));}}; const abort=new AbortController(); initImagePaste(root,abort.signal,()=>enabled,file=>{accepted.push(file);return true;}); const paste=(items=[],extra={})=>{let prevented=false; const event={defaultPrevented:false,target:null,clipboardData:{items,files:[]},preventDefault(){prevented=true;},...extra}; handlers.get('paste')?.(event);return prevented;}; return {root,abort,accepted,paste,disable(){enabled=false;}}; } const photo={name:'paste.png',type:'image/png',size:1024}; const item=(file=photo)=>({kind:'file',type:file.type,getAsFile:()=>file}); test('explicit image paste accepts first image once and consumes only that paste',()=>{const h=harness();assert.equal(h.paste([item(),item()]),true);assert.deepEqual(h.accepted,[photo]);}); test('text, missing clipboard and invalid image items remain untouched',()=>{const h=harness();assert.equal(h.paste([{kind:'string',type:'text/plain'}]),false);assert.equal(h.paste([], {clipboardData:null}),false);assert.equal(h.paste([{kind:'file',type:'image/png',getAsFile:()=>null}]),false);assert.equal(h.accepted.length,0);}); test('editable fields and contenteditable keep native paste',()=>{for(const target of [{isContentEditable:true},{closest:()=>({})}]){const h=harness();assert.equal(h.paste([item()],{target}),false);assert.equal(h.accepted.length,0);} const h=harness();h.root.activeElement={closest:()=>({})};assert.equal(h.paste([item()]),false);}); test('blocked mode, previously handled event and teardown do not read images',()=>{const h=harness();h.disable();assert.equal(h.paste([item()]),false);const other=harness();assert.equal(other.paste([item()],{defaultPrevented:true}),false);other.abort.abort();assert.equal(other.paste([item()]),false);assert.equal(other.accepted.length,0);}); test('files-only clipboard fallback accepts image but not nonimages',()=>{const h=harness();assert.equal(h.paste([],{clipboardData:{files:[{type:'text/plain'},photo]}}),true);assert.deepEqual(h.accepted,[photo]);}); test('open dialogs and editable composed paths leave paste untouched',()=>{const h=harness();h.root.querySelector=()=>({open:true});assert.equal(h.paste([item()]),false);h.root.querySelector=()=>null;assert.equal(h.paste([item()],{composedPath:()=>[{isContentEditable:true}]}),false);assert.equal(h.accepted.length,0);}); test('JPEG after text and unusable image is accepted without reading HTML',()=>{const h=harness(),jpg={name:'dog.jpg',type:'image/jpeg',size:20};assert.equal(h.paste([{kind:'string',type:'text/html',getAsString(){throw Error('not allowed');}},{kind:'file',type:'image/png',getAsFile:()=>null},item(jpg)]),true);assert.deepEqual(h.accepted,[jpg]);}); test('rejected image does not consume native paste',()=>{let callback;const abort=new AbortController();const root={activeElement:null,addEventListener(_,fn){callback=fn;}};initImagePaste(root,abort.signal,()=>true,()=>false);let prevented=false;callback({target:null,clipboardData:{items:[item()]},preventDefault(){prevented=true;}});assert.equal(prevented,false);}); test('keyboard shortcut hint is part of the desktop-only manual notices', async()=>{ const {readFile}=await import('node:fs/promises');const source=await readFile(new URL('../src/ui/EntryNotices.ts',import.meta.url),'utf8'); assert.match(source,/\(hover: hover\) and \(pointer: fine\)/); assert.match(source,/desktop\?\.matches \? 3 : 2/); });