import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { parse } from '@vue/compiler-sfc' import ts from 'typescript' import * as vue from 'vue' import * as model from '../src/views/pl/quota-cert/model' import { useClientColumnFilters } from '../src/composables/useClientColumnFilters' import { usePersistedListColumns } from '../src/composables/usePersistedListColumns' function deferred() { let resolve!: (value: any) => void const promise = new Promise((r) => { resolve = r }) return { promise, resolve } } function harness() { const reads: string[] = [], writes: any[] = [], parses: any[] = [], messages: string[] = [] let confirmSwitch = true let pendingOptions: ReturnType | undefined const env = { ...Object.fromEntries( Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default') ), ...model, useClientColumnFilters, usePersistedListColumns, useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }), useRoute: () => ({ query: {} }), useUserStore: () => ({ accessToken: 'synthetic', getUserInfo: {} }), onMounted: () => {}, onBeforeUnmount: () => {}, fetchGetMenuCodes: async () => ({ all: true }), ElMessage: { warning: (s: string) => messages.push(s), success: (s: string) => messages.push(s) }, ElMessageBox: { confirm: async () => { if (!confirmSwitch) throw new Error('cancel') } }, quotaCertApi: { list: async (kind: string) => { reads.push(kind) return pendingOptions ? pendingOptions.promise : [] }, get: async (kind: string, id: string) => ({ ...model.newCert(kind as any), id, certNo: 'EXISTING', attachmentUrls: 'a.png,b.png' }), save: async (kind: string, body: any, id?: string) => { writes.push({ kind, body, id }) }, recognize: (kind: string, url: string) => { const pending = deferred() parses.push({ kind, url, ...pending }) return pending.promise } } } const script = parse( readFileSync(new URL('../src/views/pl/quota-cert/index.vue', import.meta.url), 'utf8') ).descriptor.scriptSetup!.content const source = ts.createSourceFile('page.ts', script, ts.ScriptTarget.Latest, true) const statements = source.statements.filter((s) => !ts.isImportDeclaration(s)) const names = statements.flatMap((s) => ts.isVariableStatement(s) ? s.declarationList.declarations .filter((d) => ts.isIdentifier(d.name)) .map((d) => d.name.getText(source)) : ts.isFunctionDeclaration(s) && s.name ? [s.name.text] : [] ) const code = ts.transpile(statements.map((s) => s.getText(source)).join('\n'), { target: ts.ScriptTarget.ESNext }) const scope = vue.effectScope() const state = scope.run(() => new Function(...Object.keys(env), `${code}; return {${names.join(',')}}`)(...Object.values(env)) ) state.formRef.value = { validate: async () => true, clearValidate: () => {} } return { state, reads, writes, parses, messages, deferOptions: () => { pendingOptions = deferred() return pendingOptions }, cancelSwitch: () => { confirmSwitch = false }, stop: () => scope.stop() } } test('type changes immediately while options load and stale options cannot enter a later form', async () => { const h = harness(), s = h.state try { await s.edit() const options = h.deferOptions() const switching = s.changeFormKind('transport') assert.equal(s.formKind.value, 'transport') await s.changeFormKind('purchase') options.resolve([{ id: 'old-option' }]) await switching assert.equal(s.formKind.value, 'purchase') assert.deepEqual(s.purchases.value, []) } finally { h.stop() } }) test('new form type changes without closing dialog or changing the list, and drives save', async () => { const h = harness(), s = h.state try { await s.edit() assert.equal(s.formKind?.value, 'purchase') await s.changeFormKind('transport') assert.equal(s.formKind.value, 'transport') assert.equal(s.kind.value, 'purchase') assert.equal(s.formVisible.value, true) assert.ok(s.formFields.value.some(([key]: string[]) => key === 'shipperName')) assert.ok(!s.formFields.value.some(([key]: string[]) => key === 'buyerName')) s.form.value.shipperName = '合成托运单位' await s.save() assert.equal(h.writes[0].kind, 'transport') assert.equal(h.writes[0].body.shipperName, '合成托运单位') assert.equal(h.writes[0].body.buyerName, undefined) assert.equal(s.kind.value, 'transport') } finally { h.stop() } }) test('switch clears old certificate data after confirmation, preserving attachments and mode', async () => { const h = harness(), s = h.state try { await s.edit() s.form.value.certNo = 'PURCHASE-1' s.form.value.entryMode = 'image_parse' s.form.value.attachmentUrls = 'one.png' s.form.value.remark = '保留备注' await s.changeFormKind('transport') assert.equal(s.form.value.certNo, '') assert.equal(s.form.value.attachmentUrls, 'one.png') assert.equal(s.form.value.entryMode, 'image_parse') assert.equal(s.form.value.remark, '保留备注') assert.equal(s.form.value.buyerName, undefined) } finally { h.stop() } }) test('cancel type switch and existing certificate edit preserve data and original type', async () => { const h = harness(), s = h.state try { await s.edit() s.form.value.certNo = 'KEEP' h.cancelSwitch() s.selectedFormKind.value = 'transport' await s.changeFormKind('transport') assert.equal(s.formKind.value, 'purchase') assert.equal(s.selectedFormKind.value, 'purchase') assert.equal(s.form.value.certNo, 'KEEP') await s.edit({ id: 'existing' }) await s.changeFormKind('transport') assert.equal(s.formKind.value, 'purchase') assert.equal(s.form.value.attachmentUrls, 'a.png,b.png') } finally { h.stop() } }) test('single attachment is automatically selected, parsing uses form type and preserves manual edits', async () => { const h = harness(), s = h.state try { await s.edit() await s.changeFormKind('transport') s.form.value.attachmentUrls = 'one.png' await vue.nextTick() assert.equal(s.recognitionImage.value, 'one.png') s.form.value.certNo = 'MANUAL' s.edited.add('certNo') const parsing = s.recognize() assert.equal(h.parses.length, 1) assert.equal(h.parses[0].kind, 'transport') assert.equal(h.parses[0].url, 'one.png') h.parses[0].resolve({ candidates: { certNo: 'OCR', shipperName: '合成托运单位' }, invalidFields: {} }) await parsing assert.equal(s.form.value.certNo, 'MANUAL') assert.equal(s.form.value.shipperName, '合成托运单位') } finally { h.stop() } }) test('missing, removed, multiple new attachments and uploads in progress never start parsing', async () => { const h = harness(), s = h.state try { await s.edit() const empty = s.recognize() assert.equal(h.parses.length, 0) await empty s.form.value.attachmentUrls = 'one.png' await vue.nextTick() s.form.value.attachmentUrls = '' await vue.nextTick() assert.equal(s.recognitionImage.value, '') await s.recognize() s.form.value.attachmentUrls = 'one.png,two.png' s.recognitionImage.value = 'one.png' await s.recognize() s.form.value.attachmentUrls = 'one.png' s.certAttachments.value = { hasUploading: () => true } await s.recognize() assert.equal(h.parses.length, 0) await s.save() assert.equal(h.writes.length, 0) } finally { h.stop() } }) test('closed or switched forms reject delayed recognition results', async () => { for (const action of ['close', 'switch']) { const h = harness(), s = h.state try { await s.edit() s.form.value.attachmentUrls = 'one.png' await vue.nextTick() const parsing = s.recognize() assert.equal(h.parses.length, 1) if (action === 'close') { s.formVisible.value = false await s.edit() } else await s.changeFormKind('transport') h.parses[0].resolve({ candidates: { certNo: 'OLD' }, invalidFields: {} }) await parsing assert.equal(s.form.value.certNo, '') assert.equal(s.recognizing.value, false) } finally { h.stop() } } })