import test from 'node:test'; import assert from 'node:assert/strict'; import { DEFAULT_SETTINGS } from '../lib/defaults.js'; const { initializeMenus } = await import('../background/menus.js').catch(()=>({})); function event(){const listeners=[];return {addListener:f=>listeners.push(f),emit:async(...args)=>{for(const f of listeners)await f(...args);}};} function fixture(mode='advanced') { let settings={...DEFAULT_SETTINGS,contextMenuMode:mode}; const items=new Map(), launches=[];let refreshes=0; const browser={runtime:{},storage:{onChanged:event()},menus:{onShown:event(),onHidden:event(),onClicked:event(), removeAll:async()=>items.clear(),create:(item,callback)=>{items.set(item.id,{...item});callback();}, update:async(id,patch)=>{assert.ok(items.has(id));Object.assign(items.get(id),patch);},refresh:async()=>{refreshes++;}}}; const ready=initializeMenus({browser,load:async()=>({...settings}),onAction:async request=>launches.push(request)}); return {browser,items,launches,ready,get refreshes(){return refreshes;},set settings(value){settings={...settings,...value};}}; } test('mode controls roots, live preferences rebuild, context-only onShown needs no host permission',async()=>{ assert.equal(typeof initializeMenus,'function'); const f=fixture('off');await f.ready;assert.equal(f.items.size,0); f.settings={contextMenuMode:'simple'};await f.browser.storage.onChanged.emit({contextMenuMode:{}},'sync'); assert.equal(f.items.size,1);assert.equal([...f.items.values()][0].parentId,undefined); f.settings={contextMenuMode:'advanced'};await f.browser.storage.onChanged.emit({contextMenuMode:{}},'sync'); assert.equal([...f.items.values()].filter(i=>!i.parentId).length,1); await f.browser.menus.onShown.emit({contexts:['selection','link'],editable:false}); assert.deepEqual([...f.items.values()].filter(i=>i.parentId&&i.visible&&i.type!=='separator').map(i=>i.title),['Ask normally','Explain','Summarize','Debug code']); await f.browser.menus.onShown.emit({contexts:['link'],editable:false}); assert.deepEqual([...f.items.values()].filter(i=>i.parentId&&i.visible).map(i=>i.title),['Ask about this link','Review link safety']); assert.ok(f.refreshes>=2); }); test('click revalidates context and action, preserves source/settings snapshot',async()=>{ assert.equal(typeof initializeMenus,'function'); const f=fixture();await f.ready; await f.browser.menus.onClicked.emit({menuItemId:'ask-chatgpt:debug',selectionText:'code',linkUrl:'https://example.com'}); assert.equal(f.launches.length,1);assert.equal(f.launches[0].actionId,'debug');assert.equal(f.launches[0].source,'code'); f.settings={promptTemplate:'changed'};assert.equal(f.launches[0].settings.promptTemplate,'{query}'); await f.browser.menus.onClicked.emit({menuItemId:'ask-chatgpt:debug',linkUrl:'https://example.com'}); await f.browser.menus.onClicked.emit({menuItemId:'ask-chatgpt:translate',selectionText:'code'}); await f.browser.menus.onClicked.emit({menuItemId:'unknown',selectionText:'code'}); assert.equal(f.launches.length,1); }); test('translation rebuilds with literal label and rapid preference updates settle',async()=>{ assert.equal(typeof initializeMenus,'function'); const f=fixture();await f.ready; f.settings={showTranslate:true,translationMode:'preferred',translationLanguage:'A & B'}; await f.browser.storage.onChanged.emit({showTranslate:{}},'sync'); assert.equal(f.items.get('ask-chatgpt:translate').title,'Translate to A && B'); f.settings={contextMenuMode:'off'}; const a=f.browser.storage.onChanged.emit({contextMenuMode:{}},'sync'); f.settings={contextMenuMode:'simple'}; await Promise.all([a,f.browser.storage.onChanged.emit({contextMenuMode:{}},'sync')]); assert.equal(f.items.size,1); });