const { chromium } = require('playwright');
|
const fs=require('fs'), path=require('path'), assert=require('assert/strict');
|
const base=process.env.MOBILE_TEST_URL||'http://127.0.0.1:5173';
|
const out=path.resolve('.export_tmp/mobile-ui');fs.mkdirSync(out,{recursive:true});
|
(async()=>{
|
const browser=await chromium.launch({headless:true,channel:'msedge'});
|
const context=await browser.newContext({viewport:{width:480,height:800}});
|
await context.addInitScript(() => {
|
if (navigator.mediaDevices) Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {
|
value: () => Promise.reject(new DOMException('测试设备未配置相机', 'NotFoundError')),
|
});
|
});
|
const page=await context.newPage(),errors=[],calls=[],submissions=[];
|
page.on('pageerror',e=>errors.push(e.message));
|
let restricted=false, firstInbound=true,permissionFails=false,failStockMore=true;
|
const permissions=['mes:pro-scan-in:inbound','mes:pro-scan-in:query','mes:pro-bag-code:query','mes:pro-bag-code:update','mes:pro-pallet:query','mes:pro-pallet:update','mes:pro-batch:query','mes:wm-material-stock:query'];
|
const event={id:31,eventId:'fixture',bagCode:'6028472024079516',status:'SUCCESS',scanType:'BAG',quantity:1,batchCode:'PB-TEST',receivedTime:'2026-10-10 08:30:00'};
|
await context.route('**/*',async r=>{
|
const u=new URL(r.request().url()),p=u.pathname;
|
if(u.origin!==base){return r.abort();}
|
if(!p.startsWith('/admin-api/'))return r.continue();
|
calls.push(p+u.search);
|
let data=null;
|
if(p.endsWith('/auth/login'))data={accessToken:'fixture-token',refreshToken:'fixture-refresh',userId:7,expiresTime:Date.now()+3600000};
|
else if(p.endsWith('/get-permission-info')){
|
if(permissionFails)return r.fulfill({json:{code:500,msg:'测试:菜单暂时不可用'}});
|
const mobile=u.searchParams.get('terminalType')==='mobile';
|
data={user:{id:7,nickname:'现场测试员',username:'fixture'},roles:['worker'],permissions:restricted?[]:permissions,menus:mobile?['inbound','bags','pallets','batches','stock'].map((key,i)=>({id:i+1,path:'/mobile/'+key,name:key,visible:true,component:'mobile/'+key})):[]};
|
}else if(p.endsWith('/warehouse/simple-list'))data=[{id:1,code:'WIP_VIRTUAL_WAREHOUSE',name:'生产暂存仓'}];
|
else if(p.endsWith('/warehouse-location/simple-list'))data=[{id:2,name:'生产一区',frozen:false}];
|
else if(p.endsWith('/warehouse-area/simple-list'))data=[{id:3,name:'A01',status:0,frozen:false},{id:4,name:'停用库位',status:1,frozen:false}];
|
else if(p.endsWith('/resolve-type'))data=u.searchParams.get('code').startsWith('P')?'pallet':'bag';
|
else if(p.endsWith('/bag-code/bind-pallet')){submissions.push({binding:r.request().postDataJSON()});data=true;}
|
else if(p.endsWith('/bag-code/get-by-code'))data={id:11,code:u.searchParams.get('code')==='8888888888888888'?'6028472024079516':u.searchParams.get('code'),businessCode:'8888888888888888',itemName:'玉米种子',batchId:20,batchCode:'PB-TEST',produceDate:'2026-10-09',lineName:'一号产线'};
|
else if(p.endsWith('/pallet/get-by-code'))data={id:12,code:u.searchParams.get('code'),batchId:20,batchCode:'PB-TEST',itemName:'玉米种子'};
|
else if(p.endsWith('/scan-in/my-event'))data=null;
|
else if(p.endsWith('/scan-in/inbound')){
|
const body=r.request().postDataJSON();submissions.push(body);
|
if(firstInbound){firstInbound=false;return r.abort('timedout');}
|
data={...event,eventId:body.eventId};
|
}
|
else if(p.endsWith('/scan-in/my-page'))data={list:[event],total:1};
|
else if(p.endsWith('/material-stock/page')){
|
const more=u.searchParams.get('pageNo')==='2';
|
if(more&&failStockMore){failStockMore=false;return r.abort('timedout');}
|
data={list:[{id:more?2:1,itemName:more?'第二条库存':'玉米种子',batchCode:'PB-TEST',quantity:25,availableQuantity:22,unitMeasureName:'袋',warehouseName:'生产暂存仓',locationName:'生产一区',areaName:'A01'}],total:2};
|
}
|
else if(p.endsWith('/page'))data={list:[],total:0};
|
else if(p.endsWith('/simple-list')||p.endsWith('/list-all-simple')||p.endsWith('/get-unread-count'))data=[];
|
return r.fulfill({json:{code:0,msg:'',data}});
|
});
|
await page.goto(base+'/auth/login');await page.getByRole('radio',{name:'移动端',exact:true}).waitFor();
|
assert(await page.getByRole('radio',{name:'移动端',exact:true}).isChecked());
|
await page.getByRole('radio',{name:'PC 端',exact:true}).check();
|
await page.setViewportSize({width:390,height:800});
|
assert(await page.getByRole('radio',{name:'PC 端',exact:true}).isChecked());
|
await page.getByRole('radio',{name:'移动端',exact:true}).check();await page.setViewportSize({width:480,height:800});
|
await page.locator('input[autocomplete=username]').fill('fixture');
|
await page.locator('input[autocomplete=current-password]').fill('fixture-password');
|
await page.screenshot({path:path.join(out,'login-480.png'),fullPage:true});
|
await page.getByRole('button',{name:'登录移动端',exact:true}).click();
|
await page.waitForURL('**/mobile/home');
|
await page.getByRole('heading',{name:'从一次扫码开始'}).waitFor();
|
await page.screenshot({path:path.join(out,'home-480.png'),fullPage:true});
|
assert(calls.some(c=>c.includes('terminalType=mobile')));
|
await page.goto(base+'/mobile/inbound');
|
await page.getByRole('button',{name:'库区 请选择库区',exact:true}).click();
|
await page.getByRole('textbox',{name:'搜索库区'}).fill('生产');
|
for(let i=0;i<4;i++){await page.keyboard.press('Tab');assert(await page.getByRole('dialog',{name:'选择库区'}).evaluate(el=>el.contains(document.activeElement)));}
|
await page.keyboard.press('Escape');
|
await page.getByRole('dialog',{name:'选择库区'}).waitFor({state:'hidden'});
|
assert.equal(await page.evaluate(()=>document.body.style.overflow),'');
|
await page.getByRole('button',{name:'库区 请选择库区',exact:true}).click();
|
await page.getByRole('textbox',{name:'搜索库区'}).fill('生产');
|
await page.getByRole('button',{name:'生产一区',exact:true}).click();
|
await page.getByRole('button',{name:'库位 请选择库位',exact:true}).click();
|
assert.equal(await page.getByRole('button',{name:'停用库位',exact:true}).count(),0);
|
await page.getByRole('textbox',{name:'搜索库位'}).fill('不存在');
|
await page.getByText('没有匹配的库位').waitFor();
|
await page.getByRole('textbox',{name:'搜索库位'}).fill('A01');
|
await page.locator('#__app-loading__').waitFor({state:'detached'});
|
await page.screenshot({path:path.join(out,'location-picker-480.png'),fullPage:true});
|
await page.getByRole('button',{name:'A01',exact:true}).click();
|
|
await page.getByRole('button',{name:'打开摄像头扫码'}).click();
|
await page.getByRole('alert').waitFor();
|
await page.getByRole('textbox',{name:'码值 / 溯源链接'}).fill('https://example.test/trace?code=6028472024079516');
|
await page.getByRole('button',{name:'识别码值',exact:true}).click();
|
await page.getByRole('heading',{name:'核对入库对象'}).waitFor();
|
await page.screenshot({path:path.join(out,'inbound-confirm-480.png'),fullPage:true});
|
assert.equal(submissions.length,0,'Recognition must not submit inventory');
|
await page.getByRole('button',{name:'确认入库',exact:true}).click();
|
await page.getByRole('heading',{name:'核实上次入库结果'}).waitFor();
|
assert.equal(submissions.length,1);
|
await page.reload();
|
await page.getByRole('heading',{name:'核实上次入库结果'}).waitFor();
|
await page.getByRole('button',{name:'核实结果',exact:true}).click();
|
await page.getByText('尚未查到记录。请稍后核实,或使用原操作号重试。').waitFor();
|
await page.getByRole('button',{name:'使用原操作号重试'}).click();
|
await page.getByRole('heading',{name:'入库成功',exact:true}).waitFor();
|
assert.equal(submissions.length,2);assert.deepEqual(submissions[0],submissions[1],'Timeout retry must preserve the entire payload');
|
await page.getByRole('link',{name:'查看作业记录'}).click();
|
await page.getByText('查询本人记录').waitFor();
|
await page.screenshot({path:path.join(out,'records-480.png'),fullPage:true});
|
assert(calls.some(c=>c.includes('/my-page')&&c.includes('receivedTime')));
|
await page.goto(base+'/mobile/inbound');
|
await page.getByRole('textbox',{name:'码值 / 溯源链接'}).fill('8888888888888888');
|
await page.getByRole('button',{name:'识别码值',exact:true}).click();
|
await page.getByRole('heading',{name:'核对入库对象'}).waitFor();
|
assert.equal(submissions.length,2,'Business-code recognition must not submit inventory');
|
await page.getByRole('button',{name:'确认入库',exact:true}).click();
|
await page.getByRole('heading',{name:'入库成功',exact:true}).waitFor();
|
assert.equal(submissions[2].bagCode,'8888888888888888');
|
await page.goto(base+'/mobile/bags');
|
await page.getByRole('button',{name:'开始袋码绑定托盘'}).click();
|
await page.getByRole('textbox',{name:'码值 / 溯源链接'}).fill('PALLET-TEST');
|
await page.getByRole('button',{name:'识别码值',exact:true}).click();
|
await page.getByText('逐袋采集',{exact:true}).waitFor();
|
await page.getByRole('textbox',{name:'码值 / 溯源链接'}).fill('6028472024079516');
|
await page.getByRole('button',{name:'识别码值',exact:true}).click();
|
await page.getByText('已采集 1 袋,尚未提交绑定').waitFor();
|
await page.getByRole('button',{name:'识别码值',exact:true}).click();
|
await page.getByText('本次已经采集该袋码').waitFor();
|
assert.equal(submissions.length,3);
|
await page.getByRole('button',{name:'确认绑定',exact:true}).click();
|
await page.getByText('绑定成功,已更新对象的绑定关系。').waitFor();
|
assert.deepEqual(submissions[3],{binding:{ids:[11],palletId:12}});
|
await page.goto(base+'/mobile/stock');await page.getByText('查询库存',{exact:true}).waitFor();
|
await page.getByRole('button',{name:'查看数量明细'}).click();
|
await page.getByText('在库数量',{exact:true}).waitFor();
|
await page.screenshot({path:path.join(out,'stock-480.png'),fullPage:true});
|
await page.getByRole('textbox',{name:'品种名称',exact:true}).fill('玉米');
|
await page.getByRole('button',{name:'查询库存',exact:true}).click();
|
await page.getByRole('button',{name:'查询库存',exact:true}).waitFor();
|
await page.getByRole('textbox',{name:'品种名称',exact:true}).fill('尚未查询');
|
await page.getByRole('button',{name:'加载更多',exact:true}).click();
|
await page.getByRole('alert').waitFor();
|
await page.getByRole('button',{name:'加载更多',exact:true}).click();
|
await page.getByText('第二条库存',{exact:true}).waitFor();
|
const stockMore=calls.filter(c=>c.includes('/material-stock/page')&&c.includes('pageNo=2'));
|
assert.equal(stockMore.length,2);assert(stockMore.every(c=>c.includes('itemName=%E7%8E%89%E7%B1%B3')));
|
const savedScroll=await page.evaluate(()=>{window.scrollTo(0,200);return window.scrollY;});
|
await page.getByRole('button',{name:'返回上一页'}).click();
|
await page.getByRole('link',{name:'作业',exact:true}).click();
|
await page.getByRole('link',{name:'库存查询 品种、批次与库位',exact:true}).click();
|
assert.equal(await page.getByRole('textbox',{name:'品种名称',exact:true}).inputValue(),'尚未查询');
|
await page.waitForFunction(y=>Math.abs(window.scrollY-y)<2,savedScroll);
|
await page.getByText('第二条库存',{exact:true}).waitFor();
|
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>window.innerWidth),false);
|
restricted=true;await page.goto(base+'/mobile/stock');
|
await page.waitForURL('**/mobile/home?forbidden=1');await page.getByText('暂无移动端可用功能').waitFor();
|
restricted=false;permissionFails=true;await page.goto(base+'/mobile/home');
|
await page.waitForURL('**/auth/login?loadError=1');
|
await page.getByText('菜单加载失败,请重新登录后重试。').waitFor();
|
permissionFails=false;
|
await page.getByRole('button',{name:'登录移动端',exact:true}).click();await page.waitForURL('**/mobile/home');
|
await page.goto(base+'/mobile/mine');await Promise.all([page.waitForResponse(r=>r.url().includes('terminalType=pc')),page.getByRole('button',{name:'切换至 PC 端'}).click()]);
|
await page.waitForFunction(()=>sessionStorage.getItem('mom-terminal')==='pc');
|
|
assert(calls.some(c=>c.includes('terminalType=pc')),'Switch must reload PC menus');
|
await page.evaluate(()=>sessionStorage.setItem('mom-terminal','mobile'));await page.goto(base+'/mobile/mine');
|
await page.getByRole('button',{name:'退出登录',exact:true}).click();await page.waitForURL('**/auth/login');
|
assert.equal(await page.evaluate(()=>Object.keys(sessionStorage).some(k=>k.startsWith('mom-mobile-'))),false);
|
await page.goto(base+'/mobile/inbound');await page.waitForURL('**/auth/login?redirect=*');
|
const pc=await browser.newContext({viewport:{width:1280,height:800}});await pc.route('**/admin-api/**',r=>r.fulfill({json:{code:0,data:[]}}));
|
const desktop=await pc.newPage();await desktop.goto(base+'/auth/login');
|
assert(await desktop.getByRole('radio',{name:'PC 端',exact:true}).isChecked());
|
await desktop.screenshot({path:path.join(out,'login-pc.png'),fullPage:true});
|
assert.deepEqual(errors,[]);
|
await browser.close();console.log(JSON.stringify({passed:true,calls,submissions},null,2));
|
|
})().catch(e=>{console.error(e);process.exit(1);});
|