/** 来源业务详情加载、错误及重试;接口为隔离模拟。 */ import assert from 'node:assert/strict' import path from 'node:path' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import vue from '@vitejs/plugin-vue' import { createServer } from 'vite' const require = createRequire(import.meta.url) const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/Administrator/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') const root = fileURLToPath(new URL('../', import.meta.url)) const id = 'virtual:source-documents-proof' const harness = `import {createApp,h} from 'vue';import {createPinia} from 'pinia';import ElementPlus from 'element-plus';import 'element-plus/dist/index.css';import Source from '/src/views/business/businesses/SourceDocuments.vue';import {useUserStore} from '/src/store/user.ts';const pinia=createPinia();createApp({setup(){window.__sourceUser=useUserStore();window.__sourceUser.userInfo={permissions:['business:businesses:update','payroll:prepare']};return()=>h(Source,{businessId:12})}}).use(pinia).use(ElementPlus).mount('#app')` const server = await createServer({ configFile: false, root, resolve: { alias: { '@': path.join(root, 'src') } }, define: { 'import.meta.env.VITE_API_BASE_URL': JSON.stringify('/api') }, plugins: [vue(), { name: 'source-documents-proof', resolveId: value => value === id ? '\0' + id : undefined, load: value => value === '\0' + id ? harness : undefined, configureServer(instance) { instance.middlewares.use('/__source-proof', async (_request, response) => { response.setHeader('Content-Type', 'text/html; charset=utf-8') response.end(await instance.transformIndexHtml('/__source-proof', '
')) }) }, }], server: { host: '127.0.0.1', port: 0 } }) let browser, failNext = false, failDetail = false, failQuantity = false, repaired = false, repairRequests = 0 try { await server.listen() browser = await chromium.launch({ headless: true, channel: 'msedge' }) const page = await browser.newPage() await page.route(url => url.pathname.startsWith('/api/'), async route => { const url = new URL(route.request().url()) if (url.pathname.endsWith('/missing-price')) { repairRequests++ const body = route.request().postDataJSON() assert.equal(body.revision, 'a'.repeat(64)) assert.equal(body.evidence, '已核对原单') repaired = true return route.fulfill({ json: { code: 200, message: 'success', data: { document_id: 2, parent_status: 'APPLIED' } } }) } if (url.pathname.endsWith('/source-quantities') && failQuantity) { failQuantity = false return route.fulfill({ status: 500, json: { code: 500, message: '加载失败' } }) } if (url.pathname.endsWith('/details') && failDetail) { failDetail = false return route.fulfill({ status: 500, json: { code: 500, message: '加载失败' } }) } if (url.pathname.endsWith('/source-documents') && failNext) { failNext = false return route.fulfill({ status: 500, json: { code: 500, message: '加载失败' } }) } const data = url.pathname.endsWith('/details') ? { list: [ { source_id: '12', state: 'CHANGED', before: { product_id: '9', quantity: '2', amount: '100', price: '50', price_type: '含税' }, after: { product_id: '9', quantity: '3', amount: '150', price: '50', price_type: '未税' }, changes: [{ field: 'quantity', label: '数量', before: '2', after: '3' }] }, { source_id: '13', state: 'ADDED', before: null, after: { product_id: '9', quantity: '1', amount: '50', price: '10.12345', price_type: '未税' }, changes: [] }, ], total: 2 } : url.pathname.endsWith('/readiness') ? { status: 'PENDING', reasons: [], customer_type: 'personal', products: [], products_total: 0, eligible_receipts_snapshot: null } : url.pathname.endsWith('/source-quantities') ? { list: [{ product_id: 9, product_name: '测试管材', spec_model: 'DN300', unit: '根', order_quantity: '10.00', shipment_quantity: '4.00' }], total: 1 } : { list: [{ id: 1, entity: 'businesses', source_no: 'SO-001', current_source_no: 'SO-002', status: 'CONFLICT', changes: [{ field: 'amount', label: '金额', before: '100', after: '200' }] }, { id: 2, entity: 'shipment_items', source_no: 'L-1', snapshot_state: 'CHANGED', price: '10.1234', price_type: 'TAX_EXCLUDED', price_repair_revision: repaired ? null : 'a'.repeat(64), changes: [] }, { id: 3, entity: 'shipment_items', source_no: 'L-2', snapshot_state: 'MISSING', price: '12.3400', price_type: 'TAX_INCLUDED', changes: [] }, { id: 4, entity: 'shipment_items', source_no: 'L-3', snapshot_state: 'UNAVAILABLE', price: '5.00', changes: [] }, { id: 5, entity: 'receipts', source_no: 'RC-001', status: 'CONFLICT', reason: 'PENDING_SOURCE_CUSTOMER_CHANGED', changes: [{ field: 'customer_id', label: '客户标识', before: '8', after: '10' }] }, { id: 6, entity: 'receipts', source_no: 'RC-002', status: 'CONFLICT', reason: 'PENDING_SOURCE_OUT_OF_RANGE', changes: [{ field: 'date', label: '日期', before: '2026-03-14', after: '2026-02-28' }] }], total: 21 } await route.fulfill({ json: { code: 200, message: 'success', data } }) }) await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/__source-proof`) await page.getByText('SO-001').waitFor() await page.getByText('SO-002').waitFor() await page.getByText('10.1234(未税)').waitFor() await page.getByText('12.3400(含税)').waitFor() await page.getByText('5.00(税别待确认)').waitFor() await page.evaluate(() => { window.__sourceUser.userInfo.permissions = ['business:businesses:update'] }) assert.equal(await page.getByRole('button', { name: '核对单价' }).count(), 0) await page.evaluate(() => { window.__sourceUser.userInfo.permissions = ['business:businesses:update', 'payroll:prepare'] }) await page.getByRole('button', { name: '核对单价' }).click() await page.getByRole('button', { name: '取消' }).click() assert.equal(repairRequests, 0) await page.getByRole('button', { name: '核对单价' }).click() await page.getByPlaceholder('填写核对依据').fill('已核对原单') await page.getByRole('button', { name: '确认补齐' }).click() await page.getByText('来源单价已补齐').waitFor() assert.equal(repairRequests, 1) const quantityRow = page.getByRole('row').filter({ hasText: '测试管材' }) await quantityRow.getByText('10.00', { exact: true }).waitFor() await quantityRow.getByText('4.00', { exact: true }).waitFor() await page.getByText('金额:100 → 200').waitFor() await page.getByText('待核对单据的来源客户已变化,原关联已保留,请核对原单').waitFor() await page.getByText('客户标识:8 → 10').waitFor() await page.getByText('待核对单据的来源日期已早于接入起点,原关联已保留,请核对原单').waitFor() await page.getByText('日期:2026-03-14 → 2026-02-28').waitFor() failQuantity = true await page.reload() await page.getByText('SO-001').waitFor() await page.locator('.quantity-section').getByText('列表加载失败,请重试').waitFor() await page.locator('.quantity-section').getByRole('button', { name: '重新加载' }).click() await page.getByRole('row').filter({ hasText: '测试管材' }).getByText('10.00', { exact: true }).waitFor() await page.getByText('来源明细已变化,待核对').waitFor() await page.getByText('最新完整快照中没有此明细').waitFor() await page.getByText('完整快照不可用,显示已接入明细').waitFor() await page.getByRole('button', { name: '查看明细' }).first().click() const dialog = page.getByRole('dialog', { name: '来源明细对照' }) await dialog.getByText('数量:2 → 3').waitFor() await dialog.getByText('产品 9,数量 1,单价 10.12345(未税),金额 50').waitFor() await dialog.getByText('新增', { exact: true }).waitFor() await dialog.locator('.el-dialog__headerbtn').click() failDetail = true await page.setViewportSize({ width: 375, height: 800 }) await page.getByText('客户标识:8 → 10').waitFor() assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), true) await page.getByRole('button', { name: '查看明细' }).first().click() await dialog.getByText('列表加载失败,请重试').waitFor() await dialog.getByRole('button', { name: '重新加载' }).click() await dialog.getByText('数量:2 → 3').waitFor() await dialog.locator('.el-dialog__headerbtn').click() assert.match(await page.locator('.source-section').innerText(), /可参与平账回款.*待确认/) failNext = true await page.locator('.el-pagination button.btn-next').click() await page.getByText('列表加载失败,请重试').waitFor() assert.equal(await page.getByText('SO-001').count(), 0) await page.getByRole('button', { name: '重新加载' }).click() await page.getByText('SO-001').waitFor() assert.equal(await page.getByText('列表加载失败,请重试').count(), 0) process.stdout.write('来源详情加载失败、清除旧数据和重试通过。\n') } finally { await browser?.close() await server.close() }