feat: add advanced context actions and optional translation
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user