PayrollManagementSystem/frontend/e2e/ledger-dialog-regression.mjs
liuwenxin 02fb06e0af fix(前端): 修复续算金额文案和长弹窗越界
跨月续算时将历史匹配回款与指导成本标为计算依据,避免页面误称为本次金额,并明确本期应付为续算差额。\n\n统一弹窗顶部间距与最大高度计算,保留正文滚动和底部操作区;新增隔离浏览器回归覆盖首次计提、续算状态及多种视口。\n\n验证:pnpm build;node frontend/e2e/ledger-dialog-regression.mjs。
2026-11-02 14:24:08 +08:00

128 lines
7.9 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 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()
}