PayrollManagementSystem/frontend/e2e/source-documents.mjs
liuwenxin 1e53792ad6 feat(T+): 完成数据贯通与自动接入闭环
打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。
新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。
补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
2026-11-05 18:47:13 +08:00

134 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 来源业务详情加载、错误及重试;接口为隔离模拟。 */
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', '<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><div id="app"></div><script type="module" src="/@id/__x00__virtual:source-documents-proof"></script></html>'))
}) },
}], 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()
}