129 lines
6.3 KiB
JavaScript
129 lines
6.3 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const path = require('node:path');
|
|
const source = fs.readFileSync(path.join(__dirname, '../../static/js/admin_routing.js'), 'utf8');
|
|
|
|
function snapshot(online = false){
|
|
return {ok:true, username:'alice', presence:{online},
|
|
groups:[{id:1,name:'综合客服'},{id:2,name:'售后组'}],
|
|
agents:[{username:'alice',name:'客服甲',group_id:1,group_name:'综合客服',online,active_count:1},
|
|
{username:'bob',name:'客服乙',group_id:2,group_name:'售后组',online:true,active_count:0}],
|
|
sessions:[{id:7,group_id:1,group_name:'综合客服',owner_username:'alice',owner_name:'客服甲',status:'active',version:3}],
|
|
events:[]};
|
|
}
|
|
|
|
function desk(){
|
|
const elements = new Map(), calls = [], hooks = {};
|
|
function element(id){
|
|
if(!elements.has(id)) elements.set(id, {value:'',innerHTML:'',textContent:'',hidden:false,disabled:false,
|
|
dataset:{},attributes:{},listeners:{},classList:{toggle(){},add(){},remove(){}},
|
|
setAttribute(name,value){this.attributes[name]=value;},
|
|
addEventListener(name,listener){this.listeners[name]=listener;}});
|
|
return elements.get(id);
|
|
}
|
|
const context = vm.createContext({window:{},document:{getElementById:element},console});
|
|
vm.runInContext(source,context);
|
|
const api = context.window.AdminRouting.create({username:'alice',
|
|
request:async(url,options)=>{
|
|
const data = options && options.body ? JSON.parse(options.body) : undefined;
|
|
calls.push({url,data});
|
|
if(hooks.request) return hooks.request(url,data);
|
|
if(url === '/api/admin/routing') return snapshot();
|
|
return {ok:true,online:data && data.online};
|
|
},changed(){calls.push({changed:true});}});
|
|
return {api,element,calls,hooks};
|
|
}
|
|
|
|
test('loading the desk shows offline without silently enabling automatic assignment',async()=>{
|
|
const app = desk();
|
|
await app.api.refresh();
|
|
assert.match(app.element('routing-presence-text').textContent,/离线/);
|
|
assert.equal(app.calls.filter(item=>item.data && item.data.online === true).length,0);
|
|
assert.match(app.element('routing-members').innerHTML,/客服乙/);
|
|
assert.match(app.element('routing-members').innerHTML,/售后组/);
|
|
});
|
|
|
|
test('heartbeat continues while offline and never sends an online claim',async()=>{
|
|
const app = desk();
|
|
await app.api.refresh();
|
|
await app.api.heartbeat();
|
|
const sent = app.calls.find(item=>item.url === '/api/admin/routing/presence');
|
|
assert.deepEqual(sent.data,{});
|
|
assert.match(app.element('routing-presence-text').textContent,/离线/);
|
|
});
|
|
|
|
test('failed presence change retains the confirmed status and provides retry feedback',async()=>{
|
|
const app = desk();
|
|
await app.api.refresh();
|
|
app.hooks.request=async(url)=>{ if(url.endsWith('/presence')) throw new Error('network'); return snapshot(); };
|
|
await app.api.setPresence();
|
|
assert.match(app.element('routing-presence-text').textContent,/离线/);
|
|
assert.match(app.element('routing-status').textContent,/未确认/);
|
|
assert.equal(app.element('routing-presence-toggle').disabled,false);
|
|
});
|
|
|
|
test('changing group only offers online members of that group for reassignment',async()=>{
|
|
const app = desk();
|
|
await app.api.refresh();
|
|
app.element('routing-session').value='7';
|
|
app.element('routing-session').listeners.change();
|
|
app.element('routing-target-group').value='2';
|
|
app.element('routing-target-group').listeners.change();
|
|
assert.match(app.element('routing-target-owner').innerHTML,/bob/);
|
|
assert.doesNotMatch(app.element('routing-target-owner').innerHTML,/alice/);
|
|
});
|
|
|
|
test('refresh does not silently rebase the selected assignment version',async()=>{
|
|
const app = desk();
|
|
await app.api.refresh();
|
|
app.element('routing-session').value='7';
|
|
app.element('routing-session').listeners.change();
|
|
app.element('routing-target-group').value='2';
|
|
app.element('routing-target-owner').value='bob';
|
|
const updated = snapshot(); updated.sessions[0].version=4;
|
|
app.hooks.request=async(url)=>url === '/api/admin/routing' ? updated : {ok:true};
|
|
await app.api.refresh();
|
|
await app.api.assign();
|
|
const sent=app.calls.find(item=>item.url === '/api/admin/routing/assign');
|
|
assert.equal(sent.data.version,3);
|
|
assert.equal(sent.data.username,'bob');
|
|
});
|
|
|
|
test('a late background refresh cannot override a confirmed presence switch',async()=>{
|
|
const app=desk(); await app.api.refresh();
|
|
let resolve;
|
|
app.hooks.request=async(url)=>url === '/api/admin/routing' ? new Promise(done=>{resolve=done;}) : {ok:true,online:true};
|
|
const pending=app.api.refresh();
|
|
const switching=app.api.setPresence();
|
|
await switching;
|
|
resolve(snapshot(false)); await pending;
|
|
assert.match(app.element('routing-presence-text').textContent,/在线/);
|
|
});
|
|
|
|
test('a manual refresh during a presence write cannot restore an older availability state',async()=>{
|
|
const app=desk(); await app.api.refresh();
|
|
let confirmWrite, confirmRead;
|
|
app.hooks.request=(url)=>url === '/api/admin/routing' ? new Promise(done=>{confirmRead=done;}) :
|
|
new Promise(done=>{confirmWrite=done;});
|
|
const switching=app.api.setPresence();
|
|
const refreshing=app.api.refresh();
|
|
confirmWrite({ok:true,online:true}); await switching;
|
|
if(confirmRead) confirmRead(snapshot(false));
|
|
await refreshing;
|
|
assert.match(app.element('routing-presence-text').textContent,/在线/);
|
|
});
|
|
|
|
test('an unavailable selected owner is not silently replaced with automatic queueing',async()=>{
|
|
const app=desk(); await app.api.refresh();
|
|
app.element('routing-session').value='7'; app.element('routing-session').listeners.change();
|
|
app.element('routing-target-group').value='2'; app.element('routing-target-group').listeners.change();
|
|
app.element('routing-target-owner').value='bob';
|
|
const updated=snapshot(); updated.agents[1].online=false;
|
|
app.hooks.request=async(url)=>url === '/api/admin/routing' ? updated : {ok:true};
|
|
await app.api.refresh(); await app.api.assign();
|
|
const sent=app.calls.find(item=>item.url === '/api/admin/routing/assign');
|
|
assert.equal(sent.data.username,'bob');
|
|
});
|