- Introduced tests for case creation commands, ensuring proper handling of UUIDs and payloads. - Added tests for v5 assignment functionality, validating employee identification and group assignment. - Implemented tests for remote recovery scenarios, including handling network failures and response validation. - Created tests for hardware label generation and validation, ensuring proper URL formatting and QR code generation. - Developed extensive tests for remote control functionality, covering connection isolation, command execution, and user permissions. - Added tests for workflow timer registration, ensuring correct handling of timer states and ownership validation.
159 lines
9.6 KiB
JavaScript
159 lines
9.6 KiB
JavaScript
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.setSelectionRange(text.length,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"');
|
|
});
|
|
test('save freezes input before preview responds and uses automatic billing for every line',async()=>{
|
|
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"');
|
|
assert.equal(f.nodes.filter(node=>node.tag==='select').length,0);
|
|
const actual=f.controller.getParsed().commands;
|
|
assert.equal(actual[0].settlement_choice,undefined);assert.equal(actual[1].settlement_choice,undefined);
|
|
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;
|
|
assert.equal(payload.billing_mode,'automatic');
|
|
assert.equal(payload.commands[0].settlement_choice,undefined);assert.equal(payload.commands[1].settlement_choice,undefined);
|
|
});
|
|
|
|
|
|
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'));
|
|
});
|
|
|
|
test('required quotes appear automatically after typing the preceding arguments',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
for(const text of ['/todo ', '/note ', '/tildel ct ', '/tid 30m research ', '/timer start ', '//gruppe Support ']) {
|
|
f.type(text);
|
|
assert.equal(f.input.value,text+'""');
|
|
assert.equal(f.input.selectionStart,text.length+1);
|
|
}
|
|
});
|
|
test('selecting a category inserts description quotes immediately',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
f.type('/tid 30m re');
|
|
f.nodes.find(node=>node.tag==='button' && node.children[0]?.textContent==='research').click();
|
|
assert.equal(f.input.value,'/tid 30m research ""');
|
|
assert.equal(f.input.selectionStart,f.input.value.length-1);
|
|
});
|
|
|
|
test('Enter inside command quotes keeps the closing quote on the command line',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
for(const text of ['/todo ""','/note "Tekst"','/tid 30m research "Arbejde"','//todo "Opgave" @ct fredag']) {
|
|
f.type(text);
|
|
const caret=text.indexOf('"')+1;
|
|
f.input.setSelectionRange(caret,caret);
|
|
let prevented=false;
|
|
f.input.listeners.keydown({key:'Enter',preventDefault(){prevented=true;}});
|
|
assert.equal(prevented,true);
|
|
assert.equal(f.input.value,text+'\n');
|
|
assert.equal(f.input.selectionStart,text.length+1);
|
|
}
|
|
});
|
|
test('Enter leaves ordinary prose and selections to native editing',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
f.type('Tekst "citat"');f.input.setSelectionRange(8,8);
|
|
f.input.listeners.keydown({key:'Enter',preventDefault(){assert.fail('ordinary prose intercepted');}});
|
|
f.type('/note "tekst"');f.input.setSelectionRange(8,10);
|
|
f.input.listeners.keydown({key:'Enter',preventDefault(){assert.fail('selection intercepted');}});
|
|
});
|
|
|
|
test('Enter chooses the first suggestion without arrow navigation',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
f.type('/ti');
|
|
let prevented=false;
|
|
f.input.listeners.keydown({key:'Enter',preventDefault(){prevented=true;}});
|
|
assert.equal(prevented,true);
|
|
assert.equal(f.input.value,'/tid ');
|
|
assert.equal(f.calls.length,0);
|
|
});
|
|
test('Enter honours an explicitly selected suggestion',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
f.type('/ti');
|
|
const press=key=>f.input.listeners.keydown({key,preventDefault(){}});
|
|
press('ArrowDown');press('ArrowDown');press('Enter');
|
|
assert.equal(f.input.value,'/tildel ');
|
|
});
|
|
test('Shift Enter does not accept a suggestion',async()=>{
|
|
const f=await fixture((url,body)=>response(200,{commands:body.commands}));
|
|
f.type('/ti');
|
|
f.input.listeners.keydown({key:'Enter',shiftKey:true,preventDefault(){assert.fail('Shift Enter intercepted');}});
|
|
assert.equal(f.input.value,'/ti');
|
|
});
|