import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import vm from 'node:vm' import ts from 'typescript' // 执行真实交付函数,仅替换浏览器宿主与 HTTP 边界。 const source = readFileSync( new URL('../src/views/m/crm/contract/print/deliverContractFile.ts', import.meta.url), 'utf8' ) const code = ts.transpileModule( source.replace(/^import .*$/gm, '').replace('export async function', 'async function'), { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None } } ).outputText function harness({ wechat = true, marked = false, env = true, navigation = 'success', delayedSdk = false, pendingBridge = false } = {}) { const calls = { saved: 0, uploaded: 0, navigated: [] as string[] } const events = new EventTarget() const script = new EventTarget() const mini = { getEnv(cb: (value: { miniprogram: boolean }) => void) { if (pendingBridge) { // JSSDK 1.6.0:桥接就绪后才读取宿主注入的环境标记。 events.addEventListener('WeixinJSBridgeReady', () => cb({ miniprogram: marked }), { once: true }) return } setTimeout(() => cb({ miniprogram: env }), 1) }, navigateTo(options: any) { calls.navigated.push(options.url) if (navigation === 'success') options.success() if (navigation === 'fail') options.fail({ errMsg: 'page not found' }) } } const window: any = { wx: delayedSdk ? undefined : { miniProgram: mini } } const context = vm.createContext({ window, navigator: { userAgent: wechat ? 'iPhone MicroMessenger/8.0' : 'Safari' }, document: { querySelector: () => (delayedSdk ? script : null), addEventListener: events.addEventListener.bind(events), removeEventListener: events.removeEventListener.bind(events) }, setTimeout: (fn: () => void, ms: number) => setTimeout(fn, Math.min(ms, 30)), clearTimeout, File, encodeURIComponent, FileSaver: { saveAs() { calls.saved++ } }, isInMiniProgramWebView: () => marked, CRM_UPLOAD_BIZ_TYPE: { CONTRACT_ATTACHMENT: 'contract' }, buildAttachmentUploadFormData: (file: File) => file, fetchUploadAttachment: async () => { calls.uploaded++ return { data: { fileUrl: 'contracts/a.pdf' } } }, fetchAttachmentDownloadUrl: async () => ({ data: { downloadUrl: 'https://res.example.com/a.pdf?signature=x' } }) }) vm.runInContext(code + '\nglobalThis.deliver = deliverContractFile', context) return { calls, deliver: (isActive?: () => boolean) => context.deliver(new Blob(['%PDF-1.3']), '测试合同.pdf', isActive), ready() { window.wx = { miniProgram: mini } script.dispatchEvent(new Event('load')) }, bridgeReady() { marked = true events.dispatchEvent(new Event('WeixinJSBridgeReady')) } } } test('iPhone WeChat without synchronous markers waits for getEnv and opens native document page', async () => { const h = harness() assert.equal(await h.deliver(), 'mini-program-requested') assert.equal(h.calls.saved, 0) assert.equal(h.calls.uploaded, 1) assert.match(h.calls.navigated[0], /^\/pages\/common\/contract-document\?url=/) }) test('ordinary browser still requests a browser download despite a JSSDK shell', async () => { const h = harness({ wechat: false }) assert.equal(await h.deliver(), 'browser-download') assert.equal(h.calls.saved, 1) assert.equal(h.calls.uploaded, 0) }) test('waits for the real JSSDK ordering: bridge ready then injected environment marker', async () => { const h = harness({ pendingBridge: true }) const result = h.deliver() assert.equal(h.calls.saved, 0) assert.equal(h.calls.uploaded, 0) h.bridgeReady() assert.equal(await result, 'mini-program-requested') assert.equal(h.calls.uploaded, 1) assert.equal(h.calls.navigated.length, 1) }) test('waits for an already loading JSSDK before delivering', async () => { const h = harness({ delayedSdk: true }) const result = h.deliver() h.ready() assert.equal(await result, 'mini-program-requested') assert.equal(h.calls.saved, 0) }) test('unconfirmed WeChat environment does not silently fall back to browser downloads', async () => { const h = harness({ env: false }) await assert.rejects(h.deliver(), /小程序/) assert.equal(h.calls.saved, 0) assert.equal(h.calls.uploaded, 0) }) test('missing JSSDK times out without downloading or uploading', async () => { const h = harness({ delayedSdk: true }) await assert.rejects(h.deliver(), /微信文档接口未就绪/) h.ready() assert.equal(h.calls.saved, 0) assert.equal(h.calls.uploaded, 0) }) test('closing while environment is pending prevents file upload and navigation', async () => { const h = harness() let active = true const result = h.deliver(() => active) active = false assert.equal(await result, undefined) assert.equal(h.calls.saved, 0) assert.equal(h.calls.uploaded, 0) assert.equal(h.calls.navigated.length, 0) }) for (const navigation of ['fail', 'silent']) { test( `native navigation ${navigation} rejects instead of reporting success`, { timeout: 200 }, async () => { const h = harness({ marked: true, navigation }) await assert.rejects(h.deliver(), /文档/) assert.equal(h.calls.saved, 0) } ) }