// Component interaction check with simulated API data; never creates production tasks.
|
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');
|
const 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/one-click-schedule-20261014');
|
fs.mkdirSync(output, { recursive: true });
|
const modal = `import {defineComponent,h,reactive,onMounted} from 'vue';
|
export function useVbenModal(options){const state=reactive({open:true,locked:false,confirmDisabled:true,...options});
|
const api={getData:()=>({workOrderIds:[1,2]}),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));
|
const Modal=defineComponent({props:['title'],setup(props,{slots}){return()=>state.open?h('section',{class:'qa-modal'},[
|
h('header',props.title),h('div',{class:'qa-content'},slots.default?.()),h('footer',[
|
h('button',{class:'qa-close',disabled:state.locked,onClick:()=>api.close()},'取消'),
|
h('button',{class:'qa-confirm',disabled:state.locked||state.confirmDisabled,onClick:()=>options.onConfirm()},state.locked?'正在排产…':state.confirmText)])]):null}});return[Modal,api]}`;
|
const api = `export async function previewBatchSchedule(request){window.qa.previewCalls++;await new Promise(r=>setTimeout(r,120));
|
const blocked=window.qa.mode==='blocked',late=window.qa.mode==='late';return {canExecute:!blocked,previewHash:'test-hash',previewTime:'2026-10-14 15:00:00',warnings:late?['工单WO-001预计完成时间晚于交期']:[],
|
orders:[{workOrderId:1,workOrderCode:'WO-001',quantity:100,taskCount:blocked?0:2,requestDate:'2026-10-16 00:00:00',endTime:'2026-10-16 12:00:00',late,error:blocked?'WO-001:工序成型没有可用工作站及有效设备产能,请先维护':undefined},{workOrderId:2,workOrderCode:'WO-002',quantity:200,taskCount:blocked?0:2,requestDate:'2026-10-19 00:00:00',endTime:'2026-10-19 13:00:00',late:false,error:blocked?'WO-002:工序包装缺少设备产能,请先维护':undefined}],
|
tasks:blocked?[]:[{workOrderId:1,workOrderCode:'WO-001',processId:1,processName:'成型',workstationName:'成型工作站A',machineryNames:'成型设备A01',quantity:100,hourlyCapacity:50,duration:2,startTime:'2026-10-16 09:00:00',endTime:'2026-10-16 11:00:00'},{workOrderId:1,workOrderCode:'WO-001',processId:2,processName:'包装',workstationName:'包装工作站B',machineryNames:'包装设备B01',quantity:100,hourlyCapacity:100,duration:1,startTime:'2026-10-16 11:00:00',endTime:'2026-10-16 12:00:00'},{workOrderId:2,workOrderCode:'WO-002',processId:1,processName:'成型',workstationName:'成型工作站A',machineryNames:'成型设备A01',quantity:200,hourlyCapacity:100,duration:2,startTime:'2026-10-19 09:00:00',endTime:'2026-10-19 11:00:00'},{workOrderId:2,workOrderCode:'WO-002',processId:2,processName:'包装',workstationName:'包装工作站B',machineryNames:'包装设备B01',quantity:200,hourlyCapacity:100,duration:2,startTime:'2026-10-19 11:00:00',endTime:'2026-10-19 13:00:00'}]};}
|
export async function executeBatchSchedule(request){window.qa.executeCalls++;window.qa.lastRequest=request;await new Promise(r=>setTimeout(r,200));if(window.qa.mode==='executeError')throw new Error('排产数据已变化,请重新生成预览后确认');return{taskIds:[1,2,3,4]}}`;
|
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:#f4f6f8;font-family:Microsoft YaHei,sans-serif;color:#172033}.qa-modal{width:calc(100% - 32px);max-width:1200px;margin:20px auto;background:white;border-radius:8px}.qa-modal header{padding:18px 24px;font-size:18px;font-weight:600;border-bottom:1px solid #e8ebee}.qa-content{padding:24px}.qa-modal footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #e8ebee}.qa-modal button{padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:white;cursor:pointer}.qa-modal .qa-confirm{background:#1677ff;color:white;border-color:#1677ff}.qa-modal button:disabled{color:#8c8c8c;background:#f5f5f5;border-color:#d9d9d9;cursor:default}.space-y-5>*+*{margin-top:20px}.flex{display:flex}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-end{align-items:flex-end}.gap-3{gap:12px}.gap-2{gap:8px}.font-medium{font-weight:500}.text-sm{font-size:14px}.text-muted-foreground{color:#657083}.text-destructive{color:#cf1322}.mb-2{margin-bottom:8px}.mt-2{margin-top:8px}h3{font-size:15px;margin-top:0}p{margin:4px 0}@media(max-width:600px){.qa-content{padding:16px}.qa-modal header{padding:16px}.ant-descriptions{overflow-x:auto}}</style>
|
<div id="app"></div><script type="module" src="/qa-entry.js"></script></html>`;
|
const entry = `import{createApp,h}from'vue';import AutoSchedule from'/src/views/mes/pro/task/modules/auto-schedule.vue';window.qa={mode:new URLSearchParams(location.search).get('mode')||'valid',previewCalls:0,executeCalls:0,success:0};createApp({render:()=>h(AutoSchedule,{onSuccess:()=>window.qa.success++})}).mount('#app')`;
|
|
const server = await createServer({ root, configFile: false, appType: 'custom', cacheDir: 'node_modules/.cache/one-click-schedule-qa', server: { host: '127.0.0.1', port: 0 },
|
plugins: [{ name: 'schedule-qa', enforce: 'pre',
|
resolveId(id) { if (id === '@vben/common-ui') return '\0qa-modal'; if (id === '#/api/mes/pro/task') return '\0qa-api'; 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-entry') return entry; },
|
transform(code, id) { if (!id.endsWith('.vue')) return;
|
const { descriptor } = parse(code); const script = compileScript(descriptor, { id: 'schedule-qa', inlineTemplate: true });
|
return { code: ts.transpileModule(script.content, { compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } }).outputText, map: null };
|
},
|
configureServer(server) { server.middlewares.use((request,response,next)=>{if(request.url?.startsWith('/?')||request.url==='/'){response.setHeader('Content-Type','text/html;charset=utf-8');response.end(html)}else next()}); },
|
}], optimizeDeps: { entries: [], noDiscovery: true, include: ['vue', 'ant-design-vue', 'dayjs'] } });
|
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:1000}});
|
page.on('pageerror',error=>errors.push(error.message));
|
const url=`http://127.0.0.1:${server.httpServer.address().port}`;
|
await page.goto(url);await page.getByText('工单校验',{exact:true}).waitFor();
|
assert.equal(await page.locator('.qa-confirm').isEnabled(),true);
|
assert.equal(await page.evaluate(()=>window.qa.previewCalls),1,'Opening modal must generate exactly one preview');
|
await page.screenshot({path:path.join(output,'desktop-preview.png'),fullPage:true});
|
await page.locator('.ant-picker').click();
|
await page.locator('td[title="2026-10-20"]').click();
|
await page.locator('.ant-picker-ok button').click();
|
await page.waitForTimeout(150);
|
assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Changed earliest time must invalidate preview');
|
await page.getByText('生成排产预览',{exact:true}).click();await page.getByText('工单校验',{exact:true}).waitFor();
|
await page.locator('.qa-confirm').click();
|
assert.equal(await page.locator('.qa-close').isEnabled(),false,'Cancel must be disabled while executing');
|
await page.waitForFunction(()=>window.qa.success===1);
|
assert.equal(await page.evaluate(()=>window.qa.executeCalls),1,'Confirm must execute only once');
|
await page.goto(url+'/?mode=blocked');await page.getByText('工单校验',{exact:true}).waitFor();
|
assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Configuration errors must block submission');
|
await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(output,'mobile-blocked.png'),fullPage:true});
|
await page.goto(url+'/?mode=late');await page.getByText('工单校验',{exact:true}).waitFor();
|
assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Late risk requires acknowledgement');
|
await page.getByText('我已核对并接受以上交期延期风险',{exact:true}).click();
|
assert.equal(await page.locator('.qa-confirm').isEnabled(),true);
|
await page.locator('.qa-confirm').click();await page.waitForFunction(()=>window.qa.success===1);
|
assert.equal(await page.evaluate(()=>window.qa.lastRequest.allowLate),true);
|
await page.goto(url+'/?mode=executeError');await page.getByText('工单校验',{exact:true}).waitFor();
|
await page.locator('.qa-confirm').click();await page.getByText('排产数据已变化,请重新生成预览后确认',{exact:true}).waitFor();
|
assert.equal(await page.locator('.qa-confirm').isEnabled(),false,'Failed execution must discard old preview');
|
assert.equal(await page.locator('.qa-close').isEnabled(),true,'Failure must unlock modal');
|
assert.deepEqual(errors,[]);
|
const result={passed:true,scope:'Actual scheduling component, simulated APIs and modal adapter; no production writes',checks:['initial preview','time change invalidates preview','single submit','execution locks cancel','missing config blocks','late acknowledgement','server rejection unlocks and invalidates'],screenshots:['desktop-preview.png','mobile-blocked.png']};
|
fs.writeFileSync(path.join(output,'frontend-result.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));
|
} catch (error) {
|
console.error(JSON.stringify({message:error.message,browserErrors:errors}));
|
throw error;
|
} finally { await browser?.close();await server.close(); }
|