import test from 'node:test'; import assert from 'node:assert/strict'; import { DEFAULT_SETTINGS } from '../lib/defaults.js'; const { createDialogController } = await import('../dialog/controller.js').catch(()=>({})); const { showActionDialog } = await import('../background/dialogs.js').catch(()=>({})); const payload = source=>({request:{actionId:'translate',source,settings:{...DEFAULT_SETTINGS}},preparation:{kind:'language'}}); function fixture(data=payload('hello')) { const opened=[];let closed=0;let state; const controller=createDialogController({payload:data,open:async(url)=>opened.push(url),render:s=>{state=s;},close:()=>closed++}); return {controller,opened,get state(){return state;},get closed(){return closed;}}; } test('dialog validates language, cancel never launches, source snapshots are independent',async()=>{ assert.equal(typeof createDialogController,'function'); const a=fixture(),b=fixture(payload('second')); await a.controller.submit(' ');assert.equal(a.opened.length,0);assert.match(a.state.message,/language/i); a.controller.cancel();await a.controller.submit('Spanish');assert.equal(a.opened.length,0);assert.equal(a.closed,1); await b.controller.submit('French');assert.equal(b.opened.length,1);assert.ok(new URL(b.opened[0]).searchParams.get('q').includes('second'));assert.equal(b.closed,1); }); test('long translation requires a separate explicit shortening confirmation',async()=>{ assert.equal(typeof createDialogController,'function'); const f=fixture(payload('x'.repeat(30000))); await f.controller.submit('Spanish');assert.equal(f.opened.length,0);assert.equal(f.state.kind,'shortened'); await f.controller.submit();assert.equal(f.opened.length,1);assert.ok(f.opened[0].length<=8000); }); test('pending launch blocks double submit and failure permits retry',async()=>{ assert.equal(typeof createDialogController,'function'); let count=0,release,state,closed=0; const controller=createDialogController({payload:payload('hello'),render:s=>state=s,close:()=>closed++,open:()=>{count++;return new Promise((resolve,reject)=>release={resolve,reject});}}); const pending=controller.submit('Spanish');await controller.submit('Spanish');assert.equal(count,1); release.reject(Error('fail'));await pending;assert.equal(closed,0);assert.match(state.message,/try again/i); const retry=controller.submit();release.resolve();await retry;assert.equal(count,2);assert.equal(closed,1); }); function event() { const listeners=new Set();return {addListener:f=>listeners.add(f),removeListener:f=>listeners.delete(f),emit:(...args)=>{for(const f of [...listeners])f(...args);},get size(){return listeners.size;}}; } function browserFixture() { let id=0;const tabs=[]; return {tabs,runtime:{id:'extension',getURL:p=>'moz-extension://test/'+p,onConnect:event()},windows:{create:async({url})=>{const tab={id:++id};tabs.push({url,...tab});return {tabs:[tab]};}},tabsApi:{onRemoved:event()}}; } function connect(b,tab,index=0) { const messages=[]; const port={name:'action-dialog:'+new URL(tab.url).searchParams.get('request'),sender:{id:'extension',url:tab.url,tab:{id:tab.id+index}},onMessage:event(),onDisconnect:event(),postMessage:v=>messages.push(v),disconnect(){this.onDisconnect.emit();}}; b.runtime.onConnect.emit(port);return {port,messages}; } test('handoff scopes data to matching dialog tab and releases background after acknowledgment',async()=>{ assert.equal(typeof showActionDialog,'function'); const b=browserFixture();b.tabs=b.tabsApi; // Capture opened tabs outside the browser API object. const opened=[];let id=0;b.windows.create=async({url})=>{const tab={id:++id,url};opened.push(tab);return {tabs:[tab]};}; const first=showActionDialog(b,payload('first')),second=showActionDialog(b,payload('second')); await new Promise(r=>setImmediate(r)); const bad=connect(b,opened[0],100);await new Promise(r=>setImmediate(r));assert.equal(bad.messages.length,0); const a=connect(b,opened[0]),c=connect(b,opened[1]);await new Promise(r=>setImmediate(r)); assert.equal(a.messages[0].payload.request.source,'first');assert.equal(c.messages[0].payload.request.source,'second'); a.port.onMessage.emit({type:'received'});c.port.onMessage.emit({type:'received'});await Promise.all([first,second]); assert.equal(b.runtime.onConnect.size,0);assert.equal(b.tabs.onRemoved.size,0); assert.equal(a.messages[0].payload.request.source,'first'); });