liu
2 天以前 089af38e1a85724364dc490ae5aea698aede01dc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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);});