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, /
{ 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, /]*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>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, / { 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, /确认前,已有记录保持不变/); });