import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { runInNewContext } from 'node:vm' import ts from 'typescript' import * as vue from 'vue' import { parse, compileScript } from 'vue/compiler-sfc' const source = readFileSync(new URL('./components/TransportPhotoGallery.vue', import.meta.url), 'utf8') const { descriptor } = parse(source) const code = ts.transpileModule(compileScript(descriptor, { id: 'transport-photos' }).content, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText const flush = async () => { await vue.nextTick(); await new Promise(resolve => setImmediate(resolve)); await vue.nextTick() } function harness(kind, overrides = {}) { const props = vue.reactive({ kind, orderNo: 'ORDER/1', refreshKey: 0 }), calls = [], disposers = [], created = [], revoked = [] const photos = { pickupPhotos: [{ fileId: '71', fileName: 'pickup.png', contentType: 'image/png', sizeBytes: 5 }], arrivalPhotos: [{ fileId: '72', fileName: 'arrival.png', contentType: 'image/png', sizeBytes: 6 }] } const api = { getTransportPhotos: async (...args) => { calls.push(['list', ...args]); return photos }, readTransportPhoto: async (...args) => { calls.push(['file', ...args]); return {} }, ...overrides } const context = { exports: {}, Error, URL: { createObjectURL: () => { const url = `blob:${created.length}`; created.push(url); return url }, revokeObjectURL: url => revoked.push(url) }, require(name) { if (name === 'vue') return { ...vue, onBeforeUnmount: callback => disposers.push(callback) } if (name === '../api/businessOrders') return api throw Error(name) } } runInNewContext(code, context) const state = context.exports.default.setup(props, { expose() {} }) return { state, props, calls, created, revoked, photos, dispose: () => disposers.forEach(fn => fn()) } } for (const kind of ['looseboard', 'trucking']) test(`${kind} loads both photo nodes and refreshes with URL cleanup`, async () => { const h = harness(kind); await flush() assert.deepEqual(h.calls, [['list', kind, 'ORDER/1'], ['file', kind, 'ORDER/1', 'PICKUP', '71'], ['file', kind, 'ORDER/1', 'ARRIVAL', '72']]) assert.equal(h.state.previewUrls.value.length, 2) h.props.refreshKey++; await flush() assert.equal(h.revoked.length, 2) assert.equal(h.state.previewUrls.value.length, 2) h.dispose(); assert.equal(h.revoked.length, 4) }) test('failed photo reads preserve the saved lists and allow retry', async () => { let failed = true const h = harness('looseboard', { readTransportPhoto: async () => { if (failed) throw Error('offline'); return {} } }); await flush() assert.match(h.state.error.value, /加载失败/); assert.equal(h.state.photos.value.pickupPhotos.length, 1) failed = false; await h.state.load(); await flush() assert.equal(h.state.error.value, ''); assert.equal(h.state.previewUrls.value.length, 2); h.dispose() }) test('responses after unmount do not allocate preview URLs', async () => { let finish const response = new Promise(resolve => { finish = resolve }) const h = harness('trucking', { readTransportPhoto: () => response }); await flush() h.dispose(); finish({}); await flush(); assert.equal(h.created.length, 0) }) test('merchant photo API uses order and phase routes with authenticated blob requests', async () => { const api = readFileSync(new URL('./api/businessOrders.ts', import.meta.url), 'utf8') const compiled = ts.transpileModule(api, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText const calls = [], context = { exports: {}, require: () => ({ apiRequest: request => calls.push(request), apiBlobRequest: request => calls.push(request) }) } runInNewContext(compiled, context) for (const [kind, route] of [['looseboard', 'local-looseboard-orders'], ['trucking', 'us-truck-orders']]) { context.exports.getTransportPhotos(kind, 'ORDER/1') assert.equal(calls.at(-1).url, `/merchant/v1/last-leg/${route}/ORDER%2F1/transport-photos`) context.exports.readTransportPhoto(kind, 'ORDER/1', 'ARRIVAL', '72') assert.equal(calls.at(-1).url, `/merchant/v1/last-leg/${route}/ORDER%2F1/transport-photos/ARRIVAL/72/content`) } assert.match(source, /preview-teleported/) assert.match(source, /flex-wrap: wrap/) })