优化入仓码查询入口与卡片交互
This commit is contained in:
+194
-13
@@ -58,6 +58,7 @@ async function customerPage(pageOptions = {}){
|
||||
closest(selector){
|
||||
if(selector === 'button[data-question]' && this.dataset.question) return this;
|
||||
if(selector === 'button[data-human-support]' && this.attributes['data-human-support'] != null) return this;
|
||||
if(selector === 'button[data-order-lookup]' && this.attributes['data-order-lookup'] != null) return this;
|
||||
return this.classList.contains(selector.slice(1)) ? this : this.parent && this.parent.closest(selector);
|
||||
}
|
||||
remove(){ this.isConnected=false; if(this.parent) this.parent.children = this.parent.children.filter(child => child !== this); }
|
||||
@@ -106,6 +107,7 @@ async function customerPage(pageOptions = {}){
|
||||
}
|
||||
const data = url === '/api/new_chat' ? {id:nextConvId++, welcome:'welcome'}
|
||||
: url === '/api/visitor/session' ? {ok:true,key:'1'.repeat(64)}
|
||||
: url === '/api/customer/session' ? {ok:true,authenticated:Boolean(pageOptions.customer),user:pageOptions.customer || null}
|
||||
: url === '/api/visitor/human/request' ? (pageOptions.serviceClosed ? {ok:false,closed:true} : {ok:true, session_id:7})
|
||||
: url.startsWith('/api/visitor/human/poll') ? state
|
||||
: ['/api/rate', '/api/feedback', '/api/visitor/human/send', '/api/visitor/human/close', '/api/ticket'].includes(url) || url.startsWith('/api/del_history') ? {ok:true}
|
||||
@@ -113,7 +115,7 @@ async function customerPage(pageOptions = {}){
|
||||
return {ok:true, json:async () => data};
|
||||
},
|
||||
});
|
||||
for(const file of ['support.js', 'visitor.js', 'customer.js', 'drafts.js', 'voice.js', 'chat.js']){
|
||||
for(const file of ['support.js', 'visitor.js', 'customer.js', 'order_lookup.js', 'drafts.js', 'voice.js', 'chat.js']){
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, '../../static/js', file), 'utf8'), context, {filename:file});
|
||||
if(file === 'voice.js'){
|
||||
const createVoice=context.ChatVoice.create;
|
||||
@@ -270,7 +272,7 @@ test('switching topics aborts an upload and does not erase the new conversation
|
||||
});
|
||||
app.element('upload-input').files=[{name:'old.png'}];
|
||||
app.element('upload-input').onchange();
|
||||
await app.click(app.quickButtons[0]);
|
||||
await app.click(app.quickButtons[1]);
|
||||
await flush();
|
||||
assert.equal(aborted,true);
|
||||
assert.equal(app.element('upload-btn').disabled,false);
|
||||
@@ -308,7 +310,7 @@ test('clearing history blocks new conversations until its timed request settles'
|
||||
assert.equal(app.calls.filter(call => call.url === '/api/new_chat').length,0);
|
||||
await app.runTimers(10000);
|
||||
assert.match(app.element('historyWrap').textContent,/保留历史/);
|
||||
await app.click(app.quickButtons[0]);
|
||||
await app.click(app.quickButtons[1]);
|
||||
assert.equal(app.calls.filter(call => call.url === '/stream_chat').length,1);
|
||||
});
|
||||
|
||||
@@ -379,13 +381,16 @@ test('recalling an earlier reply preserves newer replies in the customer log', a
|
||||
|
||||
function response(data, status = 200){ return {ok:status >= 200 && status < 300, status, json:async () => data}; }
|
||||
|
||||
test('all three footer questions leave the human queue and send to separate AI conversations', async () => {
|
||||
test('tracking does not interrupt human support while quote and exception shortcuts start AI topics', async () => {
|
||||
const app = await customerPage();
|
||||
for(const button of app.quickButtons) await app.click(button);
|
||||
await app.click(app.quickButtons[0]);
|
||||
assert.equal(app.context.window.humanSid,7);
|
||||
assert.match(app.element('composer-status').textContent,/先返回 AI/);
|
||||
for(const button of app.quickButtons.slice(1)) await app.click(button);
|
||||
const requests = app.calls.filter(call => call.url === '/stream_chat');
|
||||
assert.equal(requests.length, 3);
|
||||
assert.equal(new Set(requests.map(call => call.body.conv_id)).size, 3);
|
||||
assert.deepEqual(requests.map(call => call.body.msg), ['按入仓码查轨迹', '欧洲海运专线报价', '包裹异常处理']);
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(new Set(requests.map(call => call.body.conv_id)).size, 2);
|
||||
assert.deepEqual(requests.map(call => call.body.msg), ['欧洲海运专线报价', '包裹异常处理']);
|
||||
assert.equal(app.calls.filter(call => call.url === '/api/visitor/human/send').length, 0);
|
||||
assert.equal(app.context.window.humanSid, 0);
|
||||
assert.equal(app.element('human-session').hidden, true);
|
||||
@@ -440,8 +445,8 @@ test('a repeated quick click while a new conversation is being created sends onl
|
||||
const app = await customerPage({human:false});
|
||||
let resolve;
|
||||
app.hooks['/api/new_chat'] = () => new Promise(done => { resolve=done; });
|
||||
app.quickButtons[0].click();
|
||||
app.quickButtons[0].click();
|
||||
app.quickButtons[1].click();
|
||||
app.quickButtons[1].click();
|
||||
assert.equal(app.calls.filter(call => call.url === '/api/new_chat').length, 1);
|
||||
resolve(response({id:12, welcome:'welcome'}));
|
||||
await flush();
|
||||
@@ -452,7 +457,7 @@ test('a late conversation creation cannot replace the newest quick question', as
|
||||
const app = await customerPage({human:false});
|
||||
let resolve, count=0;
|
||||
app.hooks['/api/new_chat'] = () => ++count === 1 ? new Promise(done => { resolve=done; }) : response({id:22, welcome:'welcome'});
|
||||
app.quickButtons[0].click();
|
||||
app.quickButtons[1].click();
|
||||
await app.click(app.quickButtons[2]);
|
||||
resolve(response({id:21, welcome:'welcome'}));
|
||||
await flush();
|
||||
@@ -619,12 +624,188 @@ test('inbound codes are the primary card identity for live replies and restored
|
||||
app.hooks['/stream_chat']=()=>streamResponse(['data: '+JSON.stringify({card})+'\n\ndata: {"d":1}\n\n']);
|
||||
await app.context.window.fillInput('INB20261029001-01');await flush();
|
||||
let heading=app.element('chatDom').querySelector('.track-no');
|
||||
assert.match(heading.innerHTML,/入仓码.*INB20261029001-01/);assert.doesNotMatch(heading.innerHTML,/FL202610290001/);
|
||||
assert.match(heading.textContent,/入仓码.*INB20261029001-01/);assert.doesNotMatch(heading.textContent,/FL202610290001/);
|
||||
assert.match(app.element('chatDom').querySelector('.order-data-fields').textContent,/订单号.*FL202610290001/);
|
||||
app.hooks.messages=()=>response([{role:'assistant',content:'__CARD__'+JSON.stringify(card),ctime:'now'}]);
|
||||
await app.click(app.element('historyWrap').children[0]);
|
||||
heading=app.element('chatDom').querySelector('.track-no');
|
||||
assert.match(heading.innerHTML,/入仓码.*INB20261029001-01/);assert.doesNotMatch(heading.innerHTML,/FL202610290001/);
|
||||
assert.match(heading.textContent,/入仓码.*INB20261029001-01/);assert.doesNotMatch(heading.textContent,/FL202610290001/);
|
||||
});
|
||||
|
||||
async function submitOrderLookup(app,reference){
|
||||
app.element('order-reference').value=reference;
|
||||
app.element('order-reference').listeners.input();
|
||||
await app.element('order-lookup-form').listeners.submit({preventDefault(){}});await flush();
|
||||
}
|
||||
|
||||
test('the lookup shortcut keeps the current conversation, draft and attachment until a real reference is submitted', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'}});
|
||||
await app.context.window.fillInput('原来的聊天');await flush();
|
||||
app.element('input-text').value='尚未发送的草稿';app.element('input-text').listeners.input();
|
||||
app.context.window.__pendingImg='/uploads/next-image.png';
|
||||
const before=app.calls.filter(call=>call.url==='/stream_chat').length;
|
||||
await app.click(app.quickButtons[0]);
|
||||
assert.equal(app.element('order-lookup').hidden,false);
|
||||
assert.equal(app.element('.left-panel').classList.contains('left-panel-hide'),false);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,before);
|
||||
assert.equal(app.element('input-text').value,'尚未发送的草稿');
|
||||
await submitOrderLookup(app,'hello');
|
||||
assert.match(app.element('order-lookup-error').textContent,/完整/);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,before);
|
||||
await submitOrderLookup(app,' INB202611040001 ');
|
||||
const last=app.calls.filter(call=>call.url==='/stream_chat').at(-1);
|
||||
assert.equal(last.body.msg,'查询入仓码 INB202611040001');
|
||||
assert.equal(last.body.conv_id,1);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/api/new_chat').length,1);
|
||||
assert.equal(app.element('order-lookup').hidden,true);
|
||||
assert.match(app.element('chatDom').textContent,/查询入仓码 INB202611040001/);
|
||||
assert.equal(app.element('input-text').value,'尚未发送的草稿');
|
||||
assert.equal(app.context.window.__pendingImg,'/uploads/next-image.png');
|
||||
});
|
||||
|
||||
test('the guest shortcut signs in before opening lookup and sends no empty order question', async () => {
|
||||
const app=await customerPage({human:false});
|
||||
app.hooks['/api/customer/login']=()=>response({ok:true,user:{id:1,name:'admin'}});
|
||||
app.element('input-text').value='游客原草稿';app.element('input-text').listeners.input();
|
||||
await app.click(app.quickButtons[0]);
|
||||
assert.equal(app.element('customer-login').hidden,false);
|
||||
assert.equal(app.element('order-lookup').hidden,true);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/api/new_chat' || call.url==='/stream_chat').length,0);
|
||||
app.element('customer-username').value='admin';app.element('customer-password').value='123456';
|
||||
await app.element('customer-login-form').listeners.submit({preventDefault(){}});await flush();
|
||||
assert.equal(app.element('order-lookup').hidden,false);
|
||||
assert.equal(app.element('input-text').value,'游客原草稿');
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,0);
|
||||
await app.click(app.element('order-lookup-cancel'));
|
||||
assert.equal(app.element('input-text').value,'游客原草稿');
|
||||
});
|
||||
|
||||
test('live and saved inbound references open a prefilled form and copy only the code', async () => {
|
||||
const copied=[];
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'},history:[{id:9,title:'历史订单',ctime:'now'}],
|
||||
clipboard:{writeText:async value=>{copied.push(value);}}});
|
||||
const card={kind:'order',no:'FL202611040001',status:'履约中',fields:[{key:'inbound',value:'INB202611040001'}],nodes:[],summary:'订单概要'};
|
||||
app.hooks['/stream_chat']=()=>streamResponse(['data: '+JSON.stringify({card})+'\n\ndata: {"d":1}\n\n']);
|
||||
await app.context.window.fillInput('INB202611040001');await flush();
|
||||
await app.click(app.element('chatDom').querySelector('.order-reference'));
|
||||
assert.equal(app.element('order-reference').value,'INB202611040001');
|
||||
await app.click(app.element('order-reference-copy'));
|
||||
assert.deepEqual(copied,['INB202611040001']);
|
||||
assert.equal(app.element('order-reference-copy').textContent,'已复制');
|
||||
await app.click(app.element('order-lookup-cancel'));
|
||||
app.hooks.messages=()=>response([{role:'assistant',content:'__CARD__'+JSON.stringify(card),ctime:'now'}]);
|
||||
await app.click(app.element('historyWrap').children[0]);
|
||||
await app.click(app.element('chatDom').querySelector('.order-reference'));
|
||||
app.hooks['/stream_chat']=()=>streamResponse(['data: '+JSON.stringify({card:{...card,nodes:[{time:'2026-11-04 12:00',text:'最新物流节点',place:''}]}})+'\n\ndata: {"d":1}\n\n']);
|
||||
await submitOrderLookup(app,'INB202611040001');
|
||||
const last=app.calls.filter(call=>call.url==='/stream_chat').at(-1);
|
||||
assert.equal(last.body.conv_id,9);assert.equal(last.body.msg,'查询入仓码 INB202611040001');
|
||||
assert.equal(app.element('chatDom').querySelectorAll('.track-card').length,2);
|
||||
assert.match(app.element('chatDom').querySelectorAll('.track-card').at(-1).textContent,/最新物流节点/);
|
||||
});
|
||||
|
||||
test('an order without an inbound code keeps its truthful order-number label and lookup kind', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'}});
|
||||
const card={kind:'order',no:'FL202611040001',status:'履约中',fields:[],nodes:[],summary:'订单概要'};
|
||||
app.hooks['/stream_chat']=()=>streamResponse(['data: '+JSON.stringify({card})+'\n\ndata: {"d":1}\n\n']);
|
||||
await app.context.window.fillInput(card.no);await flush();
|
||||
const header=app.element('chatDom').querySelector('.track-no');
|
||||
assert.match(header.textContent,/订单号/);assert.doesNotMatch(header.textContent,/入仓码/);
|
||||
await app.click(header.querySelector('.order-reference'));
|
||||
assert.equal(app.element('order-reference-label').textContent,'订单号');
|
||||
await submitOrderLookup(app,card.no);
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').at(-1).body.msg,'查询订单 '+card.no);
|
||||
});
|
||||
|
||||
test('lookup copy failure is visible and a retry succeeds without changing the typed code', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'},copyResult:false,
|
||||
clipboard:{writeText:()=>Promise.reject(new Error('denied'))}});
|
||||
await app.click(app.quickButtons[0]);
|
||||
app.element('order-reference').value='INB202611040001';app.element('order-reference').listeners.input();
|
||||
await app.click(app.element('order-reference-copy'));
|
||||
assert.equal(app.element('order-reference-copy').textContent,'复制失败');
|
||||
assert.match(app.element('order-lookup-error').textContent,/复制失败/);
|
||||
app.context.document.execCommand=()=>true;
|
||||
await app.click(app.element('order-reference-copy'));
|
||||
assert.equal(app.element('order-reference-copy').textContent,'已复制');
|
||||
assert.equal(app.element('order-lookup-error').hidden,true);
|
||||
assert.equal(app.element('order-reference').value,'INB202611040001');
|
||||
assert.equal(app.context.document.body.children.length,0);
|
||||
});
|
||||
|
||||
test('a duplicated lookup submit sends once and a failed creation keeps the reference editable', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'}});let release;
|
||||
await app.click(app.quickButtons[0]);
|
||||
app.hooks['/api/new_chat']=()=>new Promise(resolve=>{release=resolve;});
|
||||
const pending=submitOrderLookup(app,'INB202611040001');await flush();
|
||||
await app.element('order-lookup-form').listeners.submit({preventDefault(){}});
|
||||
assert.equal(app.calls.filter(call=>call.url==='/api/new_chat').length,1);
|
||||
release(response({ok:false},503));await pending;
|
||||
assert.equal(app.element('order-lookup').hidden,false);
|
||||
assert.equal(app.element('order-reference').value,'INB202611040001');
|
||||
assert.equal(app.element('order-reference').disabled,false);
|
||||
app.hooks['/api/new_chat']=()=>response({id:11,welcome:'welcome'});
|
||||
await submitOrderLookup(app,'INB202611040001');
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,1);
|
||||
});
|
||||
|
||||
test('lookup does not stop an active AI reply or clear a pending draft', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'}});
|
||||
app.hooks['/stream_chat']=()=>({ok:true,body:{getReader:()=>({read:()=>new Promise(()=>{})})}});
|
||||
await app.context.window.fillInput('正在回复的问题');await flush();
|
||||
app.element('input-text').value='下一条草稿';app.element('input-text').listeners.input();
|
||||
await app.click(app.quickButtons[0]);
|
||||
assert.equal(app.element('order-lookup').hidden,true);
|
||||
assert.equal(app.context.window.__busy,true);
|
||||
assert.match(app.element('composer-status').textContent,/等待当前操作/);
|
||||
assert.equal(app.element('input-text').value,'下一条草稿');
|
||||
await app.click(app.element('send-button'));
|
||||
});
|
||||
|
||||
test('a late shortcut sign-in cannot open a private lookup after the user changes topics', async () => {
|
||||
const app=await customerPage({human:false});let release;
|
||||
app.hooks['/api/customer/login']=()=>new Promise(resolve=>{release=resolve;});
|
||||
await app.click(app.quickButtons[0]);
|
||||
app.element('customer-username').value='admin';app.element('customer-password').value='123456';
|
||||
const signing=app.element('customer-login-form').listeners.submit({preventDefault(){}});
|
||||
await app.click(app.element('.new-chat-btn'));
|
||||
app.element('input-text').value='新话题草稿';app.element('input-text').listeners.input();
|
||||
release(response({ok:true,user:{id:1,name:'admin'}}));await signing;
|
||||
assert.equal(app.element('order-lookup').hidden,true);
|
||||
assert.equal(app.element('input-text').value,'新话题草稿');
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,0);
|
||||
});
|
||||
|
||||
test('welcome lookup uses the same form and follows the mobile keyboard viewport', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'},width:390,viewportHeight:844});
|
||||
const welcome=app.element('lookup-welcome');welcome.setAttribute('data-order-lookup','');
|
||||
app.element('chatDom').listeners.click({target:welcome});await flush();
|
||||
assert.equal(app.element('order-lookup').hidden,false);
|
||||
await app.resizeViewport(430,1,80);
|
||||
assert.equal(app.element('order-lookup').style.height,'430px');
|
||||
assert.equal(app.element('order-lookup').style.top,'80px');
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,0);
|
||||
});
|
||||
|
||||
test('a late repeated shortcut status response cannot erase the latest lookup input', async () => {
|
||||
const user={id:1,name:'admin'}, app=await customerPage({human:false,customer:user}), releases=[];
|
||||
app.hooks['/api/customer/session']=()=>new Promise(resolve=>{releases.push(resolve);});
|
||||
const first=app.quickButtons[0].click(), second=app.quickButtons[0].click();await flush();
|
||||
releases[1](response({ok:true,authenticated:true,user}));await second;await flush();
|
||||
app.element('order-reference').value='INB202611040001';app.element('order-reference').listeners.input();
|
||||
releases[0](response({ok:true,authenticated:true,user}));await first;await flush();
|
||||
assert.equal(app.element('order-reference').value,'INB202611040001');
|
||||
assert.equal(app.calls.filter(call=>call.url==='/stream_chat').length,0);
|
||||
});
|
||||
|
||||
test('closing lookup restores the shortcut instead of reopening the composer keyboard', async () => {
|
||||
const app=await customerPage({human:false,customer:{id:1,name:'admin'},width:390});
|
||||
const input=app.element('input-text');input.focus();
|
||||
await app.click(app.quickButtons[0]);
|
||||
await app.click(app.element('order-lookup-cancel'));
|
||||
assert.equal(app.context.document.activeElement,app.quickButtons[0]);
|
||||
assert.equal(input.focusCalls.length,1);
|
||||
assert.equal(app.quickButtons[0].focusCalls.at(-1).preventScroll,true);
|
||||
});
|
||||
|
||||
test('order details can expand without hiding the shipment timeline', async () => {
|
||||
|
||||
Reference in New Issue
Block a user