PayrollManagementSystem/frontend/e2e/ledger-dialog-regression.mjs

128 lines
7.9 KiB
JavaScript
Raw Normal View History

/** 隔离渲染真实组件与样式,只使用合成台账,不连接业务接口。 */
import assert from 'node:assert/strict'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
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 output = path.resolve(process.env.UI_REGRESSION_OUTPUT || path.join(root, '../.ai/ui-review-20261102/regression'))
const virtualId = 'virtual:ledger-dialog-regression'
const harness = `
import { createApp, h, ref } from 'vue'
import ElementPlus, { ElButton, ElDialog } from 'element-plus'
import 'element-plus/dist/index.css'
import '/src/assets/styles/index.css'
import LedgerCalculationDetails from '/src/components/payroll/LedgerCalculationDetails.vue'
createApp({ setup() {
const ledger = ref(null), loading = ref(false), visible = ref(false), lineCount = ref(50)
window.setLedger = (row, pending = false) => { ledger.value = row; loading.value = pending }
window.showDialog = (count) => { lineCount.value = count; visible.value = true }
window.saved = 0
return () => h('main', [
h(LedgerCalculationDetails, { ledger: ledger.value, loading: loading.value }),
h(ElDialog, { modelValue: visible.value, 'onUpdate:modelValue': value => visible.value = value,
title: '长表单回归验证', width: '680px', destroyOnClose: true }, {
default: () => Array.from({ length: lineCount.value }, (_, i) => h('p', '表单内容 ' + (i + 1))),
footer: () => h(ElButton, { type: 'primary', onClick: () => { window.saved++; visible.value = false } }, () => '保存'),
}),
])
} }).use(ElementPlus).mount('#app')
`
const server = await createServer({
configFile: false, root, cacheDir: path.join(output, 'vite-cache'), plugins: [vue(), {
name: 'ledger-dialog-regression',
resolveId: id => id === virtualId ? '\0' + virtualId : undefined,
load: id => id === '\0' + virtualId ? harness : undefined,
configureServer(server) {
server.middlewares.use('/__ui-regression', async (_request, response) => {
const html = await server.transformIndexHtml('/__ui-regression', '<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><div id="app"></div><script type="module" src="/@id/__x00__virtual:ledger-dialog-regression"></script></html>')
response.setHeader('Content-Type', 'text/html; charset=utf-8')
response.end(html)
})
},
}],
server: { host: '127.0.0.1', port: 0 },
optimizeDeps: { noDiscovery: true, include: ['vue', 'element-plus'] },
})
let browser
const checks = [], errors = [], layouts = []
try {
fs.mkdirSync(output, { recursive: true })
await server.listen()
const address = server.httpServer.address()
browser = await chromium.launch({ headless: true, channel: 'msedge' })
const page = await browser.newPage({ viewport: { width: 1440, height: 800 }, reducedMotion: 'reduce' })
page.on('pageerror', error => errors.push(error.message))
await page.route(url => url.pathname.startsWith('/api/'), () => { throw new Error('隔离回归不应访问业务接口') })
await page.goto(`http://127.0.0.1:${address.port}/__ui-regression`)
await page.waitForLoadState('networkidle')
const details = page.getByRole('region', { name: '计算依据' })
const row = { calc_type: 1, settlement_month: '2026-04', ship_date: '2026-03-01', receipt_date: '2026-03-10', payment_days: 9,
rate_text: '1%', converted_profit: '0.00', performance_amount: '0.00', payable_amount: '0.06', weight_r: 100,
snapshot_json: { engine: 'FIFO', first_accrual: false, event: { amount: '80.00', allocated_cost: '40.00' } } }
await page.evaluate(row => window.setLedger(row), row)
await details.getByText('原匹配回款(元)', { exact: true }).waitFor()
assert.match(await details.innerText(), /本期为跨月续算/)
assert.equal(await details.getByText('本次匹配回款(元)', { exact: true }).count(), 0)
for (const [label, value] of [['原匹配回款(元)', '80.00'], ['原匹配指导成本分摊(元)', '40.00'], ['本期应付(元)', '0.06'], ['计提绩效(元)', '0.00']]) {
assert.equal((await details.getByText(label, { exact: true }).locator('xpath=following-sibling::td[1]').innerText()).trim(), value)
}
await page.screenshot({ path: path.join(output, 'carry-forward.png') })
checks.push('跨月续算保留原匹配金额并明确历史依据,本期应付为 0.06 元')
await page.evaluate(row => window.setLedger({ ...row, snapshot_json: { ...row.snapshot_json, first_accrual: true } }), row)
await details.getByText('本次匹配回款(元)', { exact: true }).waitFor()
assert.equal(await details.getByText('本次指导成本分摊(元)', { exact: true }).count(), 1)
assert.doesNotMatch(await details.innerText(), /本期为跨月续算|原匹配/)
checks.push('首次计提沿用本次匹配说明')
await page.evaluate(row => window.setLedger({ ...row, snapshot_json: null }), row)
await details.getByText('单笔绩效核算', { exact: true }).waitFor()
assert.doesNotMatch(await details.innerText(), /本期为跨月续算|匹配回款/)
await page.evaluate(() => window.setLedger(null, true))
await details.getByRole('status').getByText('正在加载计算依据…').waitFor()
await page.evaluate(() => window.setLedger(null))
await details.getByText('暂无可查看的计算依据。', { exact: true }).waitFor()
checks.push('旧记录、加载中和空记录正常展示')
const dialog = page.getByRole('dialog', { name: '长表单回归验证' })
const panel = dialog.locator('.el-dialog')
for (const [width, height] of [[1440, 800], [1440, 600], [1366, 500], [768, 1024], [601, 600], [600, 600], [390, 844], [844, 390]]) {
await page.setViewportSize({ width, height })
await page.evaluate(() => window.showDialog(50))
await dialog.waitFor({ state: 'visible' })
const layout = await panel.evaluate(el => {
const rect = el.getBoundingClientRect(), footer = el.querySelector('.el-dialog__footer').getBoundingClientRect()
const body = el.querySelector('.el-dialog__body')
body.scrollTop = body.scrollHeight
return { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right, footerBottom: footer.bottom,
bodyScroll: body.scrollTop, bottomGap: parseFloat(getComputedStyle(el).getPropertyValue('--ys-dialog-bottom-gap')) }
})
assert.ok(layout.top >= 0 && layout.bottom <= height - layout.bottomGap + 1, `${width}×${height} 弹窗越界:${JSON.stringify(layout)}`)
assert.ok(layout.left >= 0 && layout.right <= width, '弹窗横向越界')
assert.ok(layout.bodyScroll > 0 && layout.footerBottom <= height, '正文应可滚动且按钮可见')
if ((width === 1440 && height === 800) || width === 390) await page.screenshot({ path: path.join(output, `long-dialog-${width}.png`) })
await dialog.getByRole('button', { name: '保存', exact: true }).click()
await dialog.waitFor({ state: 'hidden' })
layouts.push({ width, height, ...layout })
}
assert.equal(await page.evaluate(() => window.saved), layouts.length)
await page.evaluate(() => window.showDialog(1))
await dialog.waitFor({ state: 'visible' })
assert.ok((await panel.boundingBox()).height < 250, '短弹窗不应被撑满视口')
await dialog.getByRole('button', { name: '保存', exact: true }).click()
checks.push('8 种视口长弹窗正文滚动、底部留白及保存按钮可点击,短弹窗自然收缩')
assert.deepEqual(errors, [])
const result = { checks, layouts, pageErrors: errors }
fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2))
console.log(JSON.stringify(result, null, 2))
} finally {
await browser?.close()
await server.close()
}