import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import test from 'node:test' function readSource(relativePath: string) { return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8') } test('工单跳到售后:先记录待打开工单,等面板挂载后再弹窗(避免点两次)', () => { const source = readSource('../src/pages/admin/AdminWorkerPlatformPage.tsx') assert.ok(source.includes('pendingAfterSalesOrder'), '必须用 pending state 承接待打开的售后工单') assert.ok( source.includes('setPendingAfterSalesOrder(state.createOrder)'), '收到路由 state 时只记录待打开工单,不能直接调用尚未挂载的 ref', ) assert.ok( !source.includes('afterSalesPanelRef.current?.openCreateModal(state.createOrder)'), '不能在 setActiveTab 的同一次同步执行里调用 ref(此时面板还没挂载,调用会被静默吞掉)', ) assert.ok( source.includes('}, [visibleActiveTab, pendingAfterSalesOrder])'), '第二个 effect 必须在售后 Tab 激活后重跑', ) assert.ok( source.includes("if (visibleActiveTab !== 'after-sales' || !pendingAfterSalesOrder) return"), '只有切到售后 Tab 且存在待打开工单时才处理', ) assert.ok( source.includes('afterSalesPanelRef.current.openCreateModal(pendingAfterSalesOrder)'), '面板挂载后必须真正打开建单弹窗', ) }) test('售后处置弹窗:默认金额取后端建议值,并展示发单金额与拼单份数', () => { const source = readSource('../src/pages/admin/panels/AfterSalesPanel.tsx') assert.ok( source.includes('suggestedRecoveryAmount'), '默认追缴金额必须使用 suggestedRecoveryAmount', ) assert.ok( source.includes('existingRecoveryAmount > 0'), '已追缴过的售后必须沿用原有追缴金额,不能被建议值覆盖', ) assert.ok(source.includes('afterSalesCase.workOrderRewardAmount'), '整单必须展示工单发单金额') assert.ok(source.includes('afterSalesCase.shareUnitReward'), '拼单必须展示一份价格') assert.ok(source.includes('afterSalesCase.shareRewardAmount'), '拼单必须展示份额合计金额') assert.ok(source.includes('afterSalesCase.shareQuantity'), '拼单必须展示已拼份数') assert.ok(source.includes('(整单)'), '整单场景要有明确标识') }) test('拼单处置:默认一份,可输入份数并按「一份价格 × 份数」联动追缴金额;整单不出现份数输入', () => { const source = readSource('../src/pages/admin/panels/AfterSalesPanel.tsx') assert.ok(source.includes('recoveryQuantity?: number'), '表单值类型必须支持追缴份数') assert.ok(source.includes('label="追缴份数"'), '拼单必须提供追缴份数输入框') assert.ok(source.includes('name="recoveryQuantity"'), '份数输入必须绑定 recoveryQuantity') assert.ok(source.includes('min={1}'), '份数至少 1 份') assert.ok( source.includes('Math.max(1, Number(afterSalesCase.shareQuantity || 1))'), '份数上限必须是该责任份额的份数', ) assert.ok(source.includes('onValuesChange'), '份数变化必须联动追缴金额') assert.ok( source.includes('changedValues.recoveryQuantity === undefined'), '只有份数变化才重算金额,手动改金额不能被覆盖', ) assert.ok( source.includes('Number(afterSalesCase.shareUnitReward || 0)) * quantity') || source.includes('Number(afterSalesCase.shareUnitReward || 0)'), '联动金额必须按一份价格乘以份数计算', ) assert.ok(source.includes('resolutionRecoveryQuantity'), '弹窗必须回显当前追缴份数') // 份数输入必须只在 shareId 分支(拼单)渲染,整单只展示发单金额。 const shareBranch = source.indexOf('afterSalesCase.shareId ? (') const quantityLabel = source.indexOf('label="追缴份数"') assert.ok(shareBranch > 0 && quantityLabel > shareBranch, '份数输入必须在拼单分支内') const wholeOrderAlert = source.indexOf('(整单)') const nextRecoveryField = source.indexOf('label="追缴金额"', wholeOrderAlert) assert.ok(wholeOrderAlert > quantityLabel, '整单分支应在拼单分支之后') assert.ok( !source.slice(wholeOrderAlert, nextRecoveryField).includes('recoveryQuantity'), '整单分支不能包含份数输入', ) }) test('建单份额下拉:每个份额要显示份数、一份价格与合计金额', () => { const source = readSource('../src/pages/admin/panels/AfterSalesPanel.tsx') assert.ok(source.includes('`${share.worker?.displayName'), '份额下拉仍要显示打手名') assert.ok(source.includes('一份 ${formatMoney(share.unitReward)}'), '份额下拉必须显示一份价格') assert.ok(source.includes('合计 ${formatMoney(share.shareReward)}'), '份额下拉必须显示合计金额') }) test('售后单类型必须包含金额字段,否则前端读取为 undefined', () => { const source = readSource('../src/types/worker-platform.ts') for (const field of [ 'workOrderRewardAmount: number', 'sharingEnabled: boolean', 'shareUnitReward: number', 'shareRewardAmount: number', 'suggestedRecoveryAmount: number', ]) { assert.ok(source.includes(field), `WorkerAfterSalesCase 缺少 ${field}`) } })