2026-10-08 08:58:02 +02:00
|
|
|
const test=require('node:test');
|
|
|
|
|
const assert=require('node:assert/strict');
|
|
|
|
|
const fs=require('node:fs');
|
|
|
|
|
const vm=require('node:vm');
|
|
|
|
|
const parser=require('../../static/js/case-commands.js');
|
|
|
|
|
const completion=require('../../static/js/case-command-suggestions.js');
|
|
|
|
|
class Element {
|
|
|
|
|
constructor(tag='div') {this.tag=tag;this.children=[];this.dataset={};this.listeners={};this.value='';this.disabled=false;this.selectionStart=0;this.classList={toggle(){},add(){},remove(){}};}
|
|
|
|
|
append(...children){this.children.push(...children);}
|
|
|
|
|
after(){}
|
|
|
|
|
add(option){this.children.push(option);}
|
|
|
|
|
setAttribute(){}
|
|
|
|
|
replaceChildren(...children){this.children=children;}
|
|
|
|
|
addEventListener(type,handler){this.listeners[type]=handler;}
|
|
|
|
|
focus(){}
|
|
|
|
|
setRangeText(value,start,end){this.value=this.value.slice(0,start)+value+this.value.slice(end);this.selectionStart=start+value.length;}
|
|
|
|
|
setSelectionRange(start,end){this.selectionStart=start;this.selectionEnd=end;}
|
|
|
|
|
querySelectorAll(selector){const list=this.children.flatMap(child=>[child,...child.querySelectorAll(selector)]);return list.filter(child=>selector.split(',').some(tag=>child.tag===tag));}
|
|
|
|
|
querySelector(selector){if(selector.startsWith('[data-line=')) return this.children.find(child=>child.dataset.line===selector.match(/"(\d+)"/)[1]);return null;}
|
|
|
|
|
scrollIntoView(){}
|
|
|
|
|
click(){this.listeners.click?.();}
|
|
|
|
|
}
|
|
|
|
|
async function fixture(handler) {
|
|
|
|
|
const input=new Element('textarea'),button=new Element('button'),composer=new Element(),form=new Element('form');
|
|
|
|
|
form.querySelector=selector=>selector.includes('textarea') ? input : selector.includes('submit') ? button : composer;
|
|
|
|
|
const panelNodes=[],data={textContent:JSON.stringify({caseId:186})},storage=new Map(),calls=[];
|
|
|
|
|
const document={createTextNode:text=>{const node=new Element('text');node.textContent=text;return node;},readyState:'complete',getElementById:id=>id==='comment-form' ? form : id==='caseV5Data' ? data : null,createElement:tag=>{const element=new Element(tag);panelNodes.push(element);return element;}};
|
|
|
|
|
const context={document,CaseCommands:parser,CaseCommandSuggestions:completion,Option:function(text,value){const option=new Element('option');option.textContent=text;option.value=value;return option;},crypto:{randomUUID:()=>`uuid-${calls.length}`},Date,Map,setTimeout:()=>1,clearTimeout(){},sessionStorage:{getItem:key=>storage.get(key)||null,setItem:(key,value)=>storage.set(key,value),removeItem:key=>storage.delete(key)},fetch:async(url,options)=>{
|
|
|
|
|
if(url==='/api/v1/auth/me')return {ok:true,json:async()=>({id:17})};
|
|
|
|
|
if(url.includes('billing-options'))return {ok:true,json:async()=>({has_customer:true,hourly_rate:100})};
|
|
|
|
|
if(url.includes('command-mail-drafts'))return {ok:true,json:async()=>[]};
|
|
|
|
|
const body=JSON.parse(options.body);calls.push({url,body});return handler(url,body,calls);
|
|
|
|
|
}};
|
|
|
|
|
context.window=context;vm.createContext(context);
|
|
|
|
|
vm.runInContext(fs.readFileSync('static/js/case-command-composer.js','utf8'),context);
|
|
|
|
|
for(let i=0;i<10;i++)await Promise.resolve();
|
|
|
|
|
const type=text=>{input.value=text;input.selectionStart=text.length;input.listeners.input();};
|
|
|
|
|
return {controller:context.CaseCommandComposer,input,button,type,calls,nodes:panelNodes,storage};
|
|
|
|
|
}
|
|
|
|
|
const response=(status,data)=>({ok:status<400,status,json:async()=>data});
|
|
|
|
|
test('network uncertainty retries the exact command payload and UUID',async()=>{
|
|
|
|
|
let attempts=0;
|
|
|
|
|
const f=await fixture((url,body)=>{
|
|
|
|
|
if(url.endsWith('preview'))return response(200,{commands:body.commands});
|
|
|
|
|
if(++attempts===1)throw new Error('offline');
|
|
|
|
|
return response(200,{results:[{type:'time',case_id:186,id:81}]});
|
|
|
|
|
});
|
|
|
|
|
f.type('/tid 30m remote "Support"');
|
|
|
|
|
await assert.rejects(f.controller.submit(),/offline/);
|
|
|
|
|
assert.equal(f.input.readOnly,true);
|
|
|
|
|
const first=f.calls.find(call=>call.url.endsWith('execute')).body;
|
|
|
|
|
await f.controller.submit();
|
|
|
|
|
const executes=f.calls.filter(call=>call.url.endsWith('execute'));
|
|
|
|
|
assert.deepEqual(executes[1].body,first);
|
|
|
|
|
assert.equal(f.input.value,'');assert.equal(f.input.readOnly,false);
|
|
|
|
|
});
|
|
|
|
|
test('definite 409 allows editing instead of trapping the draft',async()=>{
|
|
|
|
|
const f=await fixture((url,body)=>url.endsWith('preview') ? response(200,{commands:body.commands}) : response(409,{detail:'Timer allerede startet'}));
|
|
|
|
|
f.type('/timer start "Support"');
|
|
|
|
|
await assert.rejects(f.controller.submit(),/Timer allerede/);
|
|
|
|
|
assert.equal(f.input.readOnly,false);assert.equal(f.input.value,'/timer start "Support"');
|
|
|
|
|
});
|
2026-10-08 21:36:42 +02:00
|
|
|
test('save freezes input before preview responds and uses automatic billing for every line',async()=>{
|
2026-10-08 08:58:02 +02:00
|
|
|
let release;
|
|
|
|
|
const f=await fixture((url,body)=>url.endsWith('preview') ? new Promise(resolve=>{release=()=>resolve(response(200,{commands:body.commands}));}) : response(200,{results:[]}));
|
|
|
|
|
f.type('/tid 30m remote "Support"\n/tid 30m remote "Support"');
|
2026-10-08 21:36:42 +02:00
|
|
|
assert.equal(f.nodes.filter(node=>node.tag==='select').length,0);
|
2026-10-08 08:58:02 +02:00
|
|
|
const actual=f.controller.getParsed().commands;
|
2026-10-08 21:36:42 +02:00
|
|
|
assert.equal(actual[0].settlement_choice,undefined);assert.equal(actual[1].settlement_choice,undefined);
|
2026-10-08 08:58:02 +02:00
|
|
|
const promise=f.controller.submit();assert.equal(f.input.readOnly,true);
|
|
|
|
|
release();await promise;
|
|
|
|
|
const payload=f.calls.find(call=>call.url.endsWith('execute')).body;
|
2026-10-08 21:36:42 +02:00
|
|
|
assert.equal(payload.billing_mode,'automatic');
|
|
|
|
|
assert.equal(payload.commands[0].settlement_choice,undefined);assert.equal(payload.commands[1].settlement_choice,undefined);
|
2026-10-08 08:58:02 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('grouped help inserts an example without executing it',async()=>{
|
|
|
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
|
|
|
const sections=f.nodes.filter(node=>node.className==='case-command-help-group');
|
|
|
|
|
assert.equal(sections.length,4);
|
|
|
|
|
const time=f.nodes.find(node=>node.className==='case-command-help-item' && node.children[0].textContent==='/tid');
|
|
|
|
|
time.click();
|
|
|
|
|
assert.equal(f.controller.getParsed().commands[0].type,'time');
|
|
|
|
|
assert.equal(f.controller.getParsed().commands[0].minutes,30);
|
|
|
|
|
assert.equal(f.calls.length,0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('suggestion menu continues from command to minutes to category',async()=>{
|
|
|
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
|
|
|
f.type('/ti');
|
|
|
|
|
f.nodes.find(node=>node.tag==='button' && node.className!=='case-command-help-item' && node.children[0]?.textContent==='/tid').click();
|
|
|
|
|
assert.equal(f.input.value,'/tid ');
|
|
|
|
|
f.nodes.find(node=>node.tag==='button' && node.children[0]?.textContent==='30m').click();
|
|
|
|
|
assert.equal(f.input.value,'/tid 30m ');
|
|
|
|
|
assert.ok(f.nodes.some(node=>node.tag==='button' && node.children[0]?.textContent==='research'));
|
|
|
|
|
});
|