import assert from 'node:assert/strict' import { mkdir, readFile } from 'node:fs/promises' import { chromium, webkit, devices } from 'playwright' // 本地 H5 交互回归:所有接口均由 fixture 拦截,不写真实业务库。 const origin = process.env.CONTRACT_TEST_ORIGIN || 'http://localhost:3006' const artifacts = '/tmp/shanxia-mobile-contract-print' await mkdir(artifacts, { recursive: true }) const ios = process.env.CONTRACT_TEST_IOS === '1' const browser = ios ? await webkit.launch({ headless: true }) : await chromium.launch({ headless: true, channel: 'chrome' }) const context = await browser.newContext({ ...(ios ? devices['iPhone 13'] : {}), ...(process.env.CONTRACT_TEST_IPHONE_UA ? { userAgent: devices['iPhone 13'].userAgent } : {}), ...(process.env.CONTRACT_TEST_WECHAT_UA ? { userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 27_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.65 NetType/WIFI Language/zh_CN' } : {}), viewport: { width: 375, height: 812 }, isMobile: true, hasTouch: true, deviceScaleFactor: 1 }) if (process.env.CONTRACT_TEST_BLOCK_SRCDOC === '1') { await context.addInitScript(() => { const descriptor = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'srcdoc') Object.defineProperty(HTMLIFrameElement.prototype, 'srcdoc', { ...descriptor, set(value) { if (this.id.startsWith('tinymce-')) return // 模拟WebView未完成srcdoc加载 descriptor.set.call(this, value) } }) }) } await context.addInitScript(() => { const start = () => { const root = document.documentElement if (!root) return const hide = () => { const el = document.getElementById('__vue-devtools-container__') if (el) el.style.setProperty('display', 'none', 'important') } hide() new MutationObserver(hide).observe(root, { childList: true, subtree: true }) } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start) else start() }) let releaseEditorModel let modelRequested let slowEditor = ['1', 'timeout'].includes(process.env.CONTRACT_TEST_SLOW_EDITOR) const modelStarted = new Promise((resolve) => { modelRequested = resolve }) if (slowEditor) { let heldModel = false await context.route('**/tinymce/models/dom/model.min.js', async (route) => { if (heldModel) { await route.continue() return } heldModel = true modelRequested() await new Promise((resolve) => { releaseEditorModel = resolve }) await route.continue() }) } let retryEditor = ['1', 'core'].includes(process.env.CONTRACT_TEST_RETRY_EDITOR) if (retryEditor) { let firstModel = true await context.route( process.env.CONTRACT_TEST_RETRY_EDITOR === 'core' ? '**/tinymce/tinymce.min.js' : '**/tinymce/models/dom/model.min.js', async (route) => { if (firstModel) { firstModel = false await route.fulfill({ status: 503, contentType: 'text/plain', body: 'fixture transient failure' }) } else await route.continue() } ) } const page = await context.newPage() page.setDefaultTimeout(15000) const pageErrors = [] const calls = [] page.on('pageerror', (error) => { pageErrors.push(error.message) console.error('Browser error:', error.stack) }) page.on('console', (message) => { if (message.type() === 'error') console.error('Console:', message.text()) }) const baseContract = { id: 'contract-1', contractNo: 'HT-20261017-001', contractName: '年度化工原料销售合同', customerId: 'customer-1', customerName: '宜昌测试客户', contactName: '张经理', contactMobile: '13800138000', contractType: 'sale', amount: 128600, freight: 500, barrelDeposit: 1000, amountWithTax: 130100, signDate: '2026-10-17', startDate: '2026-10-18', endDate: '2027-10-17', ownerUserId: 'user-1', ownerUserName: '测试销售', status: 'draft', canApprove: true, partyA: '宜昌测试客户', partyAContact: '张经理', partyAMobile: '13800138000', partyB: '山峡化工', partyBContact: '李经理', partyBMobile: '13900139000', paymentTerms: '月结 30 天', deliveryTerms: '送货上门', contractTerms: '按约定履行', attachmentFiles: JSON.stringify([{ ossId: '/fixtures/合同扫描件.pdf', category: 'scan' }]), remark: 'fixture 合同', createBy: 'user-1', createTime: '2026-10-17 09:00:00', products: [ { id: 'line-1', standardProductName: '工业甲醇', productName: '工业甲醇', price: 12000, subtotal: 120000, taxAmount: 13805.31, taxRate: 13, quantity: 10, unit: '吨', unitPrice: 12000, amount: 120000 } ] } const contracts = [ baseContract, { ...baseContract, id: 'contract-2', contractNo: 'HT-20261017-002', contractName: '已审批合同', status: 'approved', canApprove: false }, { ...baseContract, id: 'contract-fail', contractNo: 'HT-20261017-003', contractName: '审批失败样本', status: 'approving', canApprove: true } ] let approvals = [ { id: 'approval-1', contractId: baseContract.id, step: 1, approverId: 'user-before', approverName: '前置审批人', approvalResult: 'pending', approvalOpinion: '', createTime: '2026-10-17 10:00:00' }, { id: 'approval-2', contractId: baseContract.id, step: 2, approverId: 'user-1', approverName: '测试审批人', approvalResult: 'pending', approvalOpinion: '', createTime: '2026-10-17 10:30:00' } ] let savedTemplate = '
需方:{{partyB}}
可编辑的合同条款
手机编辑保留内容
') editor.fire('change') }) await Promise.all([ page.waitForResponse((response) => response.url().endsWith('/saveTemplate')), clickPrintButton('保存模板') ]) assert.match(savedTemplate, /手机编辑保留内容/) const save = calls.findLast((call) => call.path.endsWith('/saveTemplate')) assert.equal(save.body.id, 'contract-1') assert.equal('products' in save.body, false) await clickPrintButton('预览模式') await page .locator('.mp-contract-print') .getByRole('button', { name: '编辑模式', exact: true }) .waitFor() await clickPrintButton('编辑模式') await clickPrintButton('关闭') await page.locator('.mp-contract-print').waitFor({ state: 'hidden' }) await openPrint() assert.match( await page.evaluate(() => window.tinymce.activeEditor.getContent()), /手机编辑保留内容/ ) await clickPrintButton('关闭') await page.locator('.mp-contract-print').waitFor({ state: 'hidden' }) savedTemplate = '' await openPrint() await page.waitForFunction(() => window.tinymce.activeEditor.getContent().includes('工业甲醇')) assert.match(await page.evaluate(() => window.tinymce.activeEditor.getContent()), /产品名称/) assert.ok( await page.evaluate( () => window.tinymce.activeEditor.getBody().scrollWidth > window.tinymce.activeEditor.getWin().innerWidth ) ) await clickPrintButton('隐藏红章') await page.waitForFunction(() => !window.tinymce.activeEditor.getContent().includes('alt="印章"')) await clickPrintButton('显示红章') await page.waitForFunction(() => window.tinymce.activeEditor.getContent().includes('alt="印章"')) await page.screenshot({ path: `${artifacts}/editor-414.png` }) // 本阶段验证普通浏览器下载;之后单独切到微信宿主验证原生交付。 await page.evaluate(() => { Object.defineProperty(navigator, 'userAgent', { configurable: true, value: navigator.userAgent.replace(/MicroMessenger\/\S+|miniProgram/gi, '') }) }) for (const [label, extension] of [ ['生成 Word', '.docx'], ['生成 PDF', '.pdf'] ]) { const downloadPromise = page.waitForEvent('download', { timeout: 60000 }) await clickPrintButton(label) const download = await downloadPromise assert.ok(download.suggestedFilename().endsWith(extension)) await download.saveAs(`${artifacts}/contract${extension}`) const bytes = await readFile(`${artifacts}/contract${extension}`) assert.ok(bytes.length > 100) if (extension === '.pdf') assert.equal(bytes.subarray(0, 4).toString(), '%PDF') else assert.equal(bytes.subarray(0, 2).toString(), 'PK') } await page.evaluate(() => { // iPhone 同步环境标记缺失,但 JSSDK getEnv 能确认真实小程序宿主。 delete window.__wxjs_environment Object.defineProperty(navigator, 'userAgent', { configurable: true, value: 'Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 MicroMessenger/8.0' }) window.wx.miniProgram.getEnv = (callback) => setTimeout(() => callback({ miniprogram: true }), 30) }) failUpload = true await clickPrintButton('生成 PDF') await page.getByText('模拟上传失败', { exact: true }).first().waitFor() await page .locator('.mp-contract-print') .getByRole('button', { name: '生成 PDF', exact: true }) .waitFor() assert.match(await page.evaluate(() => window.tinymce.activeEditor.getContent()), /工业甲醇/) assert.equal(await page.evaluate(() => window.__documentNavigation), undefined) failUpload = false await clickPrintButton('生成 PDF') await page.waitForFunction(() => window.__documentNavigation) assert.match( await page.evaluate(() => window.__documentNavigation), /pages\/common\/contract-document/ ) assert.ok(calls.some((call) => call.path === '/web/admin/attachment/upload')) assert.ok(calls.some((call) => call.path === '/web/admin/attachment/download-url')) await clickPrintButton('关闭') await page.locator('.mp-contract-print').waitFor({ state: 'hidden' }) baseContract.contractNo = 'YD/X-JT20261031001' await openPrint() await page.evaluate(() => { window.tinymce.activeEditor.fire('SetContent') const close = [...document.querySelectorAll('.mp-contract-print button')].find( (button) => button.textContent.trim() === '关闭' ) close.click() }) await page.locator('.mp-contract-print').waitFor({ state: 'hidden' }) // 等待Logo回调的500ms窗口,验证关闭后没有访问已销毁的编辑器。 await page.waitForTimeout(650) assert.deepEqual(pageErrors, []) console.log( 'PASS mobile contract print: 375/414 layout, template save, preview, DOCX/PDF downloads, mini-program file bridge; fixture only' ) } finally { await page.screenshot({ path: `${artifacts}/last-state.png` }).catch(() => {}) await browser.close() }