// Actual Vue components with simulated API data; no production stock writes.
|
import assert from 'node:assert/strict';
|
import fs from 'node:fs';
|
import path from 'node:path';
|
import {createRequire} from 'node:module';
|
import {fileURLToPath} from 'node:url';
|
import {createServer} from 'vite';
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
const require=createRequire(path.join(root,'package.json'));
|
const {parse,compileScript}=require('vue/compiler-sfc'),ts=require('typescript');
|
const {chromium}=require('C:/Users/云/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
|
const output=path.resolve(root,'../scripts/spare-management-20261014');fs.mkdirSync(output,{recursive:true});
|
const modal=`import {defineComponent,h,reactive,onMounted} from 'vue';
|
export const Page=defineComponent({setup(p,{slots}){return()=>h('main',slots.default?.())}});
|
export function useVbenModal(options){
|
if(options.connectedComponent||!options.onOpenChange)return[defineComponent({setup(){return()=>null}}),{setData(){return this},open(){}}];
|
const state=reactive({open:true,locked:false,...options});const api={getData:()=>window.qa.data,setState:v=>Object.assign(state,v),lock:()=>state.locked=true,unlock:()=>state.locked=false,close:async()=>{if(options.onBeforeClose&&!options.onBeforeClose())return;state.open=false;await options.onOpenChange(false)}};
|
window.qaModal={state,api,options};onMounted(()=>options.onOpenChange(true));
|
return[defineComponent({props:['title'],setup(props,{slots}){return()=>state.open?h('section',{class:'qa-modal'},[h('header',props.title),h('div',slots.default?.()),h('footer',[h('button',{class:'qa-close',disabled:state.locked,onClick:()=>api.close()},'取消'),h('button',{class:'qa-confirm',disabled:state.locked,onClick:()=>options.onConfirm().catch(()=>{})},'确认')])]):null}}),api];}`;
|
const api=`export const movementLabels={1:'初始库存',2:'入库',3:'维修领用',4:'退回'};
|
export async function getSparePage(){return {list:window.qa.parts,total:window.qa.parts.length}}
|
export async function getSpare(){return window.qa.parts[0]}
|
export async function getSpareMovements(){return {list:window.qa.movements,total:window.qa.movements.length}}
|
export async function spareAction(type,data){window.qa.calls++;window.qa.request={type,...data};await new Promise(r=>setTimeout(r,250));if(window.qa.fail)throw Error('server rejection');return 1}
|
export async function createSpare(data){window.qa.saved=data;return 3}
|
export async function updateSpare(data){window.qa.saved=data}
|
export async function deleteSpare(){}
|
`;
|
const entry=`import {createApp,h} from 'vue';import 'ant-design-vue/dist/reset.css';
|
import Ledger from '/src/views/mes/dv/spare/index.vue';import Records from '/src/views/mes/dv/spare/modules/records.vue';import Action from '/src/views/mes/dv/spare/modules/action.vue';import Form from '/src/views/mes/dv/spare/modules/form.vue';
|
const mode=new URLSearchParams(location.search).get('mode')||'ledger';
|
const parts=[{id:1,code:'SP-001',name:'深沟球轴承',specification:'6205-2RS',unit:'件',location:'备件库 A-01',stockQuantity:10,safetyQuantity:15,status:0},{id:2,code:'SP-002',name:'传动皮带',specification:'B-1250',unit:'条',location:'备件库 B-02',stockQuantity:25,safetyQuantity:5,status:0}];
|
const movement={id:1,sparePartId:1,sparePartCode:'SP-001',sparePartName:'深沟球轴承',specification:'6205-2RS',unit:'件',type:3,quantity:4,returnedQuantity:1,returnableQuantity:3,repairCode:'WX-20261014-001',repairId:1,machineryName:'数控加工中心',receiverName:'维修员甲',remark:'更换主轴轴承',createTime:'2026-10-14 15:30:00'};
|
window.qa={parts,movements:[movement],calls:0,data:mode==='return'?{type:'return',original:movement}:mode==='form'?{}:{type:mode==='receive'?'receive':'issue',part:parts[0],repairId:1,repairCode:'WX-20261014-001'}};
|
const C=mode==='ledger'?Ledger:mode==='records'?Records:mode==='form'?Form:Action;
|
const app=createApp({render:()=>mode==='records'?h('main',[h(C)]):h(C)});app.directive('access',{});app.mount('#app');`;
|
const html=`<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>备件组件验证 · 模拟数据</title><style>body{margin:0;background:#f5f5f5;color:#1f1f1f;font:14px system-ui}main{padding:24px;background:white;margin:20px}.qa-modal{background:white;max-width:600px;margin:24px auto;padding:24px}header{font-size:20px;font-weight:600;margin-bottom:20px}footer{display:flex;gap:12px;justify-content:flex-end;padding-top:16px}footer button{padding:8px 20px}footer button:disabled{opacity:.5}@media(max-width:640px){main{margin:8px;padding:12px}.qa-modal{margin:8px;padding:16px}} .flex{display:flex}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-3{gap:12px}.gap-y-3{row-gap:12px}.mb-4{margin-bottom:16px}.mb-3{margin-bottom:12px}.mt-4{margin-top:16px}.w-full{width:100%}.w-28{width:112px}.w-36{width:144px}.text-lg{font-size:18px}.text-base{font-size:16px}.font-semibold{font-weight:600}.grid{display:grid}.gap-x-4{column-gap:16px}.ml-2{margin-left:8px}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:col-span-2{grid-column:span 2}}</style><div id="app"></div><script type="module" src="/qa-entry.js"></script></html>`;
|
const server=await createServer({root,configFile:false,appType:'custom',cacheDir:'node_modules/.cache/spare-management-qa',server:{host:'127.0.0.1',port:0},plugins:[{name:'spare-qa',enforce:'pre',
|
resolveId(id){if(id==='@vben/common-ui')return '\0qa-modal';if(id==='#/api/mes/dv/spare')return '\0qa-api';if(id==='#/api/mes/dv/repair')return '\0qa-repair';if(id==='#/api/system/user')return '\0qa-user';if(id==='/qa-entry.js')return '\0qa-entry'},
|
load(id){if(id==='\0qa-modal')return modal;if(id==='\0qa-api')return api;if(id==='\0qa-repair')return 'export async function getRepairPage(){return {list:[{id:1,code:"WX-001",status:0,machineryName:"设备A"}]}}';if(id==='\0qa-user')return 'export async function getSimpleUserList(){return [{id:1,nickname:"维修员甲"}]}';if(id==='\0qa-entry')return entry},
|
transform(code,id){if(!id.endsWith('.vue'))return;const {descriptor}=parse(code);const script=compileScript(descriptor,{id,inlineTemplate:true});return{code:ts.transpileModule(script.content,{compilerOptions:{target:ts.ScriptTarget.ESNext,module:ts.ModuleKind.ESNext}}).outputText,map:null}},
|
configureServer(s){s.middlewares.use((req,res,next)=>{if(req.url==='/'||req.url?.startsWith('/?')){res.setHeader('Content-Type','text/html;charset=utf-8');res.end(html)}else next()})}
|
}],optimizeDeps:{entries:[],noDiscovery:true,include:['vue','ant-design-vue']}});
|
await server.listen();let browser;const errors=[];
|
try{
|
browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'});const page=await browser.newPage({viewport:{width:1440,height:950}});
|
page.on('pageerror',e=>errors.push(e.message));const url='http://127.0.0.1:'+server.httpServer.address().port;
|
await page.goto(url);await page.getByText('深沟球轴承',{exact:true}).waitFor();await page.screenshot({path:path.join(output,'desktop-ledger.png'),fullPage:true});
|
await page.goto(url+'/?mode=records');await page.getByText('更换主轴轴承',{exact:true}).waitFor();await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(output,'mobile-records.png'),fullPage:true});
|
await page.goto(url+'/?mode=issue');await page.locator('.qa-confirm').waitFor();await page.locator('.qa-confirm').click();await page.waitForTimeout(100);assert.equal(await page.evaluate(()=>window.qa.calls),0,'Missing receiver and reason blocks issue');
|
await page.locator('#form_item_receiverUserId').click();await page.locator('.ant-select-item-option-content').filter({hasText:'维修员甲'}).click();
|
await page.locator('textarea').fill('更换主轴轴承');await page.locator('.ant-input-number-input').fill('11');await page.locator('.qa-confirm').click();await page.getByText('超过当前可用库存',{exact:true}).waitFor();assert.equal(await page.evaluate(()=>window.qa.calls),0);
|
await page.locator('.ant-input-number-input').fill('2');await page.setViewportSize({width:1440,height:950});await page.screenshot({path:path.join(output,'desktop-issue.png'),fullPage:true});
|
await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.calls===1);assert.equal(await page.locator('.qa-close').isEnabled(),false);await page.waitForFunction(()=>!window.qaModal.state.open);assert.equal(await page.evaluate(()=>window.qa.calls),1);
|
await page.goto(url+'/?mode=return');await page.locator('.qa-confirm').waitFor();await page.locator('textarea').fill('未使用退回');await page.locator('.ant-input-number-input').fill('4');await page.locator('.qa-confirm').click();await page.getByText('超过可退回数量',{exact:true}).waitFor();assert.equal(await page.evaluate(()=>window.qa.calls),0);
|
await page.locator('.ant-input-number-input').fill('2');await page.evaluate(()=>window.qa.fail=true);await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.calls===1&&!window.qaModal.state.locked);assert.equal(await page.locator('.qa-close').isEnabled(),true);
|
const oldKey=await page.evaluate(()=>window.qa.request.requestKey);await page.evaluate(()=>window.qa.fail=false);await page.locator('.qa-confirm').click();await page.waitForFunction(()=>!window.qaModal.state.open);assert.equal(await page.evaluate(()=>window.qa.request.requestKey),oldKey,'Failed submission retry uses same idempotency key');
|
await page.goto(url+'/?mode=form');await page.locator('.qa-confirm').waitFor();await page.locator('.qa-confirm').click();await page.waitForTimeout(80);assert.equal(await page.evaluate(()=>window.qa.saved),undefined,'Required fields block empty part creation');
|
await page.goto(url+'/?mode=issue');await page.locator('.qa-confirm').waitFor();await page.locator('#form_item_receiverUserId').click();await page.locator('.ant-select-item-option-content').filter({hasText:'维修员甲'}).click();await page.locator('textarea').fill('更换主轴轴承');await page.locator('.ant-input-number-input').fill('2');await page.locator('header').click();await page.waitForTimeout(300);await page.screenshot({path:path.join(output,'desktop-issue.png'),fullPage:true});
|
assert.deepEqual(errors,[]);const result={passed:true,scope:'Actual Vue/Ant Design components with mock APIs and modal adapter; no production writes',checks:['ledger display and shortage','records mobile display','required issue fields','issue stock limit','single submit and cancel lock','return limit','failure unlock','retry idempotency key','required part fields'],screenshots:['desktop-ledger.png','mobile-records.png','desktop-issue.png']};fs.writeFileSync(path.join(output,'frontend-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));
|
}finally{await browser?.close();await server.close()}
|