- A:出借人存在 reserved 额度占用时,身份关联与收款资料维护在出借人锁内 返回 409 loan.lender_in_flight;页面禁用并提示,修正账户弹窗误导文案 - B:回单 OCR 认领、人工回单参与人、核销按审批时资格认定,不再因原审批人 或出借员工事后离职/撤权、主数据停用而卡死;保留身份核实、版本与付款快照 一致、审批独立性及回单匹配校验;付款前环节仍校验当前资格 - 测试:隔离 PostgreSQL 合成全流程,含确定性并发交错、付款后独立性反例、 离职后经 OCR/人工复核核销、PC/手机按钮禁用;变异测试均变红 - 恢复手册:资金类数据恢复后先暂停付款,对照银行流水补齐并核对占用再开放 - design 按环节修订资格规则 注意:C(同金额回单传错批次错误核销)已复现但尚未修复,借款功能不具备 真实还款验收条件;未推送、未部署。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
112 行
8.0 KiB
JavaScript
112 行
8.0 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { mkdtempSync } = require('node:fs');
|
|
const path = require('node:path');
|
|
const { createRequire } = require('node:module');
|
|
const root = path.resolve(__dirname, '../..');
|
|
const req = createRequire(path.join(root, 'client/package.json'));
|
|
const { chromium, expect } = req('@playwright/test');
|
|
|
|
module.exports = async function runBrowser({ app, finance, ordinary, password, company, bank, prisma }) {
|
|
const output = mkdtempSync(path.join(root, 'artifacts/qa-layout/payment-feedback-loan-slim-20260917/browser-'));
|
|
const origin = await app.listen({ host: '127.0.0.1', port: 0 });
|
|
const browser = await chromium.launch({ headless: true, args: ['--disable-background-networking'] });
|
|
const checks = [];
|
|
try {
|
|
for (const mobile of [false, true]) {
|
|
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block' });
|
|
await context.route('**/*', route => route.request().url().startsWith(`${origin}/`) ? route.continue() : route.abort());
|
|
const page = await context.newPage();
|
|
const mode = mobile ? 'mobile' : 'desktop';
|
|
const target = mobile ? '/mobile/company-loans' : '/company-loans';
|
|
await page.goto(`${origin}/login?redirect=${encodeURIComponent(target)}`);
|
|
await page.getByTestId('login-identifier').fill(finance.email);
|
|
await page.getByTestId('login-password').fill(password);
|
|
await page.getByTestId('login-submit').click();
|
|
await expect(page.getByRole('heading', { name: '公司借款', exact: true })).toBeVisible();
|
|
await page.getByRole('button', { name: '登记出借人', exact: true }).click();
|
|
let dialog = page.getByRole('dialog');
|
|
await dialog.getByLabel('公司主体').selectOption(company.id);
|
|
const lenderName = `合成浏览器出借人-${mode}`;
|
|
await dialog.getByLabel('出借人姓名').fill(lenderName);
|
|
await dialog.getByLabel('收款银行').selectOption(bank.id);
|
|
await dialog.getByLabel('开户行名称').fill('合成建行');
|
|
await dialog.getByLabel('账户名', { exact: true }).fill(lenderName);
|
|
await dialog.getByLabel('收款账号', { exact: true }).fill('6'.repeat(16));
|
|
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
await page.getByRole('tab', { name: '出借人', exact: true }).click();
|
|
const lenderCard = page.locator('section').filter({ has: page.getByRole('heading', { name: `${lenderName} · ${company.name}`, exact: true }) });
|
|
await lenderCard.getByRole('button', { name: '核实身份关联' }).click();
|
|
dialog = page.getByRole('dialog');
|
|
await dialog.getByLabel('出借人对应身份').selectOption('external');
|
|
await dialog.getByLabel('说明', { exact: true }).fill('合成浏览器非员工身份核实');
|
|
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
await expect(lenderCard).toContainText('身份已核实');
|
|
await page.getByRole('button', { name: '登记借入', exact: true }).click();
|
|
dialog = page.getByRole('dialog');
|
|
const lender = await prisma.loanLender.findFirstOrThrow({ where: { name: lenderName } });
|
|
await dialog.getByLabel('出借人', { exact: true }).selectOption(lender.id);
|
|
const reference = `合成浏览器原借入-${mode}`;
|
|
await dialog.getByLabel('原借入凭据唯一引用').fill(reference);
|
|
await dialog.getByLabel('原始本金').fill('300.05');
|
|
await dialog.getByLabel('核对日未还本金').fill('300.05');
|
|
await dialog.getByLabel('实际到账日期').fill('2025-01-01');
|
|
await dialog.getByLabel('余额核对截止日').fill('2026-01-01');
|
|
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
await page.getByRole('tab', { name: '借款台账', exact: true }).click();
|
|
const loan = await prisma.companyLoan.findFirstOrThrow({ where: { origin_reference: reference } });
|
|
const card = page.locator('section').filter({ has: page.getByRole('heading', { name: `${lenderName} · ${loan.loan_no}`, exact: true }) });
|
|
await card.locator('summary').click();
|
|
for (const kind of ['agreement', 'incoming', 'accounting_confirmation']) {
|
|
const received = page.waitForResponse(r => r.url().endsWith(`/company-loans/${loan.id}/evidences/${kind}`) && r.request().method() === 'POST');
|
|
await page.locator(`[id="${loan.id}-${kind}"]`).setInputFiles({ name: 'synthetic.pdf', mimeType: 'application/pdf',
|
|
buffer: Buffer.from('%PDF-1.4\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n') });
|
|
assert.equal((await received).status(), 200);
|
|
}
|
|
await card.getByRole('button', { name: '确认期初余额' }).click();
|
|
dialog = page.getByRole('dialog');
|
|
await dialog.getByLabel('代账余额确认引用').fill('合成代账核对');
|
|
await dialog.getByRole('button', { name: '确认', exact: true }).click();
|
|
await expect(dialog).not.toBeVisible();
|
|
await expect(card).toContainText('余额已确认');
|
|
await page.getByRole('button', { name: '申请还本', exact: true }).click();
|
|
dialog = page.getByRole('dialog');
|
|
await dialog.getByLabel('出借人', { exact: true }).selectOption(lender.id);
|
|
await dialog.getByLabel(`分配 ${reference}`, { exact: true }).fill('100.05');
|
|
await dialog.getByLabel('说明', { exact: true }).fill('合成浏览器部分还本');
|
|
await dialog.getByRole('button', { name: '提交还款申请', exact: true }).click();
|
|
await expect(page.getByText('原借款分配与会计交接', { exact: true })).toBeVisible();
|
|
await expect(page.getByText('可复核:', { exact: false }).first()).toBeVisible();
|
|
await expect(page.getByText('暂无企微个人消息发送成功记录', { exact: false }).first()).toBeVisible();
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1);
|
|
assert.equal(overflow, false, `${mode} horizontal overflow`);
|
|
await page.screenshot({ path: path.join(output, `${mode}-repayment.png`), fullPage: true });
|
|
checks.push(`${mode}_real_login_register_materials_confirm_submit_and_named_progress`);
|
|
await page.goBack();
|
|
await expect(page.getByRole('heading', { name: '公司借款', exact: true })).toBeVisible();
|
|
await page.screenshot({ path: path.join(output, `${mode}-lenders-after-submit.png`), fullPage: true });
|
|
await page.getByRole('tab', { name: '出借人', exact: true }).click();
|
|
const frozenCard = page.locator('section').filter({ has: page.getByRole('heading', { name: `${lenderName} · ${company.name}`, exact: true }) });
|
|
await expect(frozenCard).toContainText('有还款正在办理');
|
|
await expect(frozenCard.getByRole('button', { name: '核实身份关联' })).toBeDisabled();
|
|
await expect(frozenCard.getByRole('button', { name: '更新收款资料' })).toBeDisabled();
|
|
checks.push(`${mode}_in_flight_repayment_disables_lender_maintenance`);
|
|
await context.close();
|
|
}
|
|
const context = await browser.newContext({ serviceWorkers: 'block' });
|
|
await context.route('**/*', route => route.request().url().startsWith(`${origin}/`) ? route.continue() : route.abort());
|
|
const page = await context.newPage();
|
|
await page.goto(`${origin}/login?redirect=%2Fcompany-loans`);
|
|
await page.getByTestId('login-identifier').fill(ordinary.email);
|
|
await page.getByTestId('login-password').fill(password);
|
|
await page.getByTestId('login-submit').click();
|
|
await expect(page.getByRole('heading', { name: '公司借款', exact: true })).not.toBeVisible();
|
|
await page.waitForURL(/\/403$/);
|
|
checks.push('ordinary_employee_cannot_open_loan_ledger');
|
|
await context.close();
|
|
console.log(JSON.stringify({ stage: 'loan_browser', status: 'PASS', checks, output: path.relative(root, output) }));
|
|
} finally { await browser.close(); }
|
|
};
|