aps-agent/tests/node/planner-data-card.test.mjs

147 lines
7.6 KiB
JavaScript
Raw Permalink Normal View History

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import test from 'node:test';
const requireWeb = createRequire(new URL('../../apps/web/package.json', import.meta.url));
const ts = requireWeb('typescript');
const React = requireWeb('react');
const { renderToStaticMarkup } = requireWeb('react-dom/server');
const source = readFileSync(new URL('../../apps/web/src/chat/PlanningDataCard.tsx', import.meta.url), 'utf8');
const compiled = ts.transpileModule(source, { compilerOptions: {
jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022,
}}).outputText;
const exported = {};
new Function('require', 'exports', compiled)(requireWeb, exported);
const block = { type: 'folder-pack', props: { canSchedule: true, projectName: '客户订单',
coverageDetail: [{ key: 'orders', count: 6 }, { key: 'equipment', count: 57 }],
files: [{ name: '订单.xlsx', okCount: 165, samples: ['type=RAW_MATERIAL,stock=0'] }],
}};
const render = extra => renderToStaticMarkup(React.createElement(exported.PlanningDataCard, {
block, active: true, formatSize: String, onSend() {}, ...extra,
}));
test('data check explains the stage and keeps raw fields out of the summary', () => {
const html = render({});
assert.match(html, /尚未生成方案/);
assert.match(html, /确认数据,生成方案/);
assert.match(html, /<details/);
assert.doesNotMatch(html, /RAW_MATERIAL|P2|工程目录数据包/);
});
test('analysis summary keeps the first screen to conclusion, key counts, and next step', () => {
const html = render({ block: { ...block, props: { ...block.props, reviewOnly: true, canSchedule: false,
analysisSummary: {
headline: '资料已读取,确认采用后才会写入项目主数据。',
status: 'review', statusLabel: '待确认采用',
summaryLine: '订单 5 · 产品和物料 45 · 加工步骤 15 · 设备 15',
metrics: [
{ key: 'orders', label: '订单', value: 5 },
{ key: 'materials', label: '产品和物料', value: 45 },
{ key: 'routing', label: '加工步骤', value: 15 },
{ key: 'equipment', label: '设备', value: 15 },
],
issues: ['喷涂没有对应技能人员'], issueOverflow: 2, nextStep: null,
},
nextActions: [{ id: 'review_missing', label: '查看缺少资料', command: '排产还缺什么', enabled: true }],
} } });
assert.match(html, /确认采用后才会写入项目主数据/);
assert.match(html, /订单 5/);
assert.match(html, /喷涂没有对应技能人员/);
assert.match(html, /另有 2 项/);
assert.match(html, /查看文件、订单与检查明细/);
assert.doesNotMatch(html, /全部工作表已逐项检查/);
assert.doesNotMatch(html, /查看缺少资料/);
});
test('old records cannot restart scheduling', () => {
const html = render({ active: false });
assert.match(html, /历史记录/);
assert.equal((html.match(/aria-label="查看[^"]+明细"/g) || []).length, 4);
assert.doesNotMatch(html, /确认数据,生成方案/);
});
test('adoption review keeps one disabled scheduling entry instead of a second call to action', () => {
const html = render({ block: { ...block, props: { ...block.props, reviewOnly: true, canSchedule: false,
entityCounts: { personnel: 12, wip: 3, sandboxOrders: 1 },
analysisSummary: {
headline: '资料已读取,确认采用后才会写入项目主数据。',
status: 'review', statusLabel: '待核对采用',
summaryLine: '订单记录 5 · 产品和物料 45 · 加工步骤 15 · 设备记录 15',
metrics: [
{ key: 'orders', label: '订单记录', value: 5 },
{ key: 'materials', label: '产品和物料', value: 45 },
{ key: 'routing', label: '加工步骤', value: 15 },
{ key: 'equipment', label: '设备记录', value: 15 },
],
issues: [], issueOverflow: 0,
nextStep: { label: '开始排产', command: '立即排产', enabled: false,
reason: '需先确认采用本次资料;采用前不会写入主数据,也不会排产。' },
recap: '现状:本次只完成核对,尚未写入主数据;请在下方确认卡核对并采用。',
},
nextActions: [{ id: 'review_missing', label: '查看缺少资料', command: '排产还缺什么', enabled: true }],
} } });
assert.match(html, /排产资料核对/);
assert.match(html, /<button[^>]*disabled[^>]*>开始排产<\/button>/);
assert.match(html, /需先确认采用本次资料/);
assert.match(html, /尚未写入主数据/);
assert.match(html, /人员 12 名/);
assert.doesNotMatch(html, /查看缺少资料/);
});
test('existing plan offers review instead of first scheduling', () => {
const html = render({ viewCommand: '查看排产结果' });
assert.match(html, /查看当前方案/);
assert.doesNotMatch(html, /确认数据,生成方案|尚未生成方案/);
});
test('unknown server state cannot offer a new schedule or label a current card historical', () => {
const html = render({ actionsReady: false });
assert.match(html, /正在核对当前方案状态/);
assert.equal((html.match(/aria-label="查看[^\"]+明细"/g) || []).length, 4);
assert.doesNotMatch(html, /历史记录|尚未生成方案/);
});
const details = props => renderToStaticMarkup(React.createElement(exported.PlanningDataDetails, {
onRecheck() {}, onClose() {}, ...props,
}));
test('detail panel renders supplied columns and ten actual rows with accessible search and pagination', () => {
const rows = Array.from({ length: 24 }, (_, index) => ({ code: `EQ-${index}`, available: index !== 0, speed: index }));
const html = details({ view: { label: '设备', columns: [
{ key: 'code', label: '设备编号' }, { key: 'available', label: '可生产' }, { key: 'speed', label: '速度' },
], rows, total: 24 }, snapshotMode: 'current', sourceSha: 'fingerprint' });
assert.match(html, /当前主数据/);
assert.match(html, /文件指纹:fingerprint/);
assert.match(html, /搜索设备/);
assert.match(html, /type="search"/);
assert.match(html, /第 1 \/ 3 页/);
assert.match(html, /设备编号/);
assert.match(html, /EQ-9/);
assert.doesNotMatch(html, /EQ-10/);
assert.match(html, /<td>否<\/td><td>0<\/td>/);
});
test('old missing details and a genuinely empty dataset are distinct', () => {
const old = details({});
assert.match(old, /旧记录缺明细,请重新检查资料/);
assert.match(old, /重新检查资料/);
assert.doesNotMatch(old, /<table/);
const empty = details({ view: { label: '订单', columns: [{ key: 'orderNo', label: '订单编号' }], rows: [], total: 0 } });
assert.match(empty, /这项资料没有记录/);
assert.doesNotMatch(empty, /旧记录缺明细/);
});
test('source snapshot warns when rows do not agree with the supplied total', () => {
const html = details({ snapshotMode: 'source', view: { label: '材料', columns: [{ key: 'code', label: '编码' }],
rows: [{ code: 'ACTUAL-ROW' }], total: 2 } });
assert.match(html, /原文件资料/);
assert.match(html, /数量与汇总不一致/);
assert.match(html, /ACTUAL-ROW/);
});
test('recovery proposal explains each existing record without acting on it', () => {
const html = render({ block: { ...block, props: { ...block.props, recoveryProposed: true,
intakeConflicts: [{ orderNo: 'ORDER-A', reason: '旧记录误入正式池', canRecover: true },
{ orderNo: 'ORDER-B', reason: '已有执行记录', canRecover: false }] } } });
assert.match(html, /ORDER-A:旧记录误入正式池/);
assert.match(html, /可在确认后整理/);
assert.match(html, /需要人工核实,暂不能自动整理/);
assert.match(html, /确认前,已有记录保持不变/);
});