Files
shanxia2.0/web-admin/scripts/quota-cert-form.test.ts
T

703 lines
23 KiB
TypeScript

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
let reject!: (reason: any) => void
const promise = new Promise<any>((r, j) => {
resolve = r
reject = j
})
return { promise, resolve, reject }
}
function harness() {
const reads: string[] = [],
writes: any[] = [],
parses: any[] = [],
messages: string[] = []
let certData: any = {}
let saveError = false
let pendingSave: ReturnType<typeof deferred> | undefined
let pendingGet: ReturnType<typeof deferred> | undefined
const user = vue.reactive({ accessToken: 'synthetic', getUserInfo: {} })
const contractRequests: any[] = []
const productRequests: any[] = []
let confirmSwitch = true
let pendingOptions: ReturnType<typeof deferred> | 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: () => user,
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: {
contracts: (keyword: string) => {
const pending = deferred()
contractRequests.push({ keyword, ...pending })
return pending.promise
},
products: (id: string) => {
const pending = deferred()
productRequests.push({ id, ...pending })
return pending.promise
},
certificateContracts: async () => [
{ id: 'old', contractNo: '历史合同', contractName: '真实标签' }
],
list: async (kind: string) => {
reads.push(kind)
return pendingOptions ? pendingOptions.promise : []
},
get: async (kind: string, id: string) =>
pendingGet
? pendingGet.promise
: {
...model.newCert(kind as any),
id,
certNo: 'EXISTING',
attachmentUrls: 'a.png',
...certData
},
save: async (kind: string, body: any, id?: string) => {
writes.push({ kind, body, id })
if (saveError) throw new Error('保存失败')
if (pendingSave) await pendingSave.promise
},
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,
user,
deferSave: () => (pendingSave = deferred()),
deferGet: () => (pendingGet = deferred()),
contractRequests,
productRequests,
setCert: (value: any) => {
certData = value
},
failSave: () => {
saveError = true
},
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()
s.form.value.attachmentUrls = 'one.png'
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()
s.form.value.attachmentUrls = 'one.png'
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.attachmentUrls = 'one.png'
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.attachmentUrls = 'one.png'
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')
} 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()
s.form.value.attachmentUrls = 'one.png'
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()
s.form.value.attachmentUrls = ''
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()
s.form.value.attachmentUrls = 'one.png'
} 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()
}
}
})
test('多合同一次保存且失败保留输入;受限可见关系只展示并省略提交', async () => {
const h = harness(),
s = h.state
const bindings = [
{
id: 'b',
contractId: 'old',
linkStatus: 'inactive',
remark: '保留',
orderNum: 4,
products: [
{
id: 'scope',
contractProductId: 'p',
scopeStatus: 'inactive',
remark: '范围',
orderNum: 2
}
]
}
]
try {
h.setCert({ contractBindings: bindings, bindingsEditable: true })
await s.edit({ id: 'existing' })
assert.deepEqual(s.form.value.contractBindings, bindings)
assert.equal(s.bindingContracts.value[0].contractNo, '历史合同')
s.addContractBinding('new')
assert.equal(s.form.value.contractBindings.length, 2)
h.failSave()
await assert.rejects(s.save(), /保存失败/)
assert.equal(h.writes.length, 1)
assert.equal(h.writes[0].body.contractBindings[0].products[0].id, 'scope')
assert.equal(s.formVisible.value, true)
assert.equal(s.form.value.contractBindings.length, 2)
s.formVisible.value = false
h.setCert({ contractBindings: bindings, bindingsEditable: false })
await s.edit({ id: 'limited' })
s.addContractBinding('forbidden')
assert.equal(s.form.value.contractBindings.length, 1)
await assert.rejects(s.save())
assert.equal('contractBindings' in h.writes[1].body, false)
} finally {
h.stop()
}
})
test('产品勾选往返保留原范围ID;关闭重开后拒绝旧合同和产品请求', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
s.addContractBinding('c')
h.productRequests[0].resolve([{ id: 'p' }, { id: 'p2' }])
await vue.nextTick()
s.form.value.contractBindings[0].products = [
{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }
]
s.setBindingProducts(0, ['p', 'p2'])
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
assert.equal(s.form.value.contractBindings[0].products[0].scopeStatus, 'inactive')
const search = s.searchBindingContracts('old')
const products = s.loadBindingProducts('c')
s.formVisible.value = false
await s.edit()
s.form.value.attachmentUrls = 'one.png'
h.contractRequests.at(-1).resolve([{ id: 'stale', contractNo: '旧合同' }])
h.productRequests.forEach((r) => r.resolve([{ id: 'stale', productName: '旧产品' }]))
await Promise.all([search, products])
assert.deepEqual(s.bindingContracts.value, [])
assert.deepEqual(s.bindingProducts.value, {})
} finally {
h.stop()
}
})
test('移除后重选同一合同保留主关联和产品范围身份', async () => {
const h = harness(),
s = h.state
try {
h.setCert({
contractBindings: [
{
id: 'binding',
contractId: 'old',
linkStatus: 'inactive',
products: [{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }]
}
]
})
await s.edit({ id: 'cert' })
s.removeContractBinding(0)
s.addContractBinding('old')
assert.equal(s.form.value.contractBindings[0].id, 'binding')
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
} finally {
h.stop()
}
})
test('后发合同搜索覆盖先发,租户切换使保存和读取旧结果失效', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
const first = s.searchBindingContracts('first'),
second = s.searchBindingContracts('second')
h.contractRequests[1].resolve([{ id: 'second', contractNo: 'SECOND', contractName: '' }])
await second
h.contractRequests[0].resolve([{ id: 'first', contractNo: 'FIRST', contractName: '' }])
await first
assert.equal(s.bindingContracts.value[0].id, 'second')
const pending = h.deferSave()
const saving = s.save()
await vue.nextTick()
h.user.accessToken = ''
pending.resolve({})
await saving
assert.equal(h.messages.includes('证件已保存'), false)
assert.equal(s.formVisible.value, false)
const get = h.deferGet(),
editing = s.edit({ id: 'old' })
h.user.getUserInfo = { tenantNo: 'other' }
get.resolve({ ...model.newCert('purchase'), contractBindings: [] })
await editing
assert.equal(s.formVisible.value, false)
} finally {
h.stop()
}
})
test('新增多合同连同证面成功提交一次,移除全部提交明确空集合', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
s.addContractBinding('one')
s.addContractBinding('two')
h.productRequests[0].resolve([{ id: 'p1' }])
h.productRequests[1].resolve([{ id: 'p2' }])
await vue.nextTick()
s.setBindingProducts(0, ['p1'])
s.setBindingProducts(1, ['p2'])
s.form.value.certNo = 'MULTI'
await s.save()
assert.equal(h.writes.length, 1)
assert.equal(h.writes[0].body.certNo, 'MULTI')
assert.deepEqual(
h.writes[0].body.contractBindings.map((b: any) => b.contractId),
['one', 'two']
)
assert.equal(s.formVisible.value, true)
h.setCert({ contractBindings: [{ id: 'b', contractId: 'one', products: [] }] })
await s.edit({ id: 'existing' })
s.removeContractBinding(0)
await s.save()
assert.deepEqual(h.writes[1].body.contractBindings, [])
} finally {
h.stop()
}
})
test('同合同产品后发请求优先,移除重加使移除前请求失效', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
s.addContractBinding('c')
const second = s.loadBindingProducts('c')
h.productRequests[1].resolve([{ id: 'new', productName: '新产品' }])
await second
h.productRequests[0].resolve([{ id: 'old', productName: '旧产品' }])
await vue.nextTick()
assert.equal(s.bindingProducts.value.c[0].id, 'new')
const old = s.loadBindingProducts('c')
s.removeContractBinding(0)
s.addContractBinding('c')
h.productRequests[2].resolve([{ id: 'removed', productName: '移除前' }])
await old
assert.equal(s.bindingProducts.value.c, undefined)
assert.equal(s.bindingProductLoading.value.c, true)
h.productRequests[3].resolve([{ id: 'readded', productName: '重加后' }])
await vue.nextTick()
assert.equal(s.bindingProducts.value.c[0].id, 'readded')
} finally {
h.stop()
}
})
test('产品加载失败可再次展开重试,加载中不能改选择', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
s.addContractBinding('c')
h.productRequests[0].reject(new Error('network'))
await vue.nextTick()
await vue.nextTick()
assert.equal(s.bindingProductLoading.value.c, false)
const retry = s.loadBindingProducts('c')
s.setBindingProducts(0, ['invalid'])
assert.deepEqual(s.form.value.contractBindings[0].products, [])
h.productRequests[1].resolve([{ id: 'p', productName: '产品' }])
await retry
s.setBindingProducts(0, ['p'])
assert.equal(s.form.value.contractBindings[0].products[0].contractProductId, 'p')
} finally {
h.stop()
}
})
test('inline binding adds an empty row, requires contract, and resets products on change', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
s.addContractBinding()
assert.equal(s.form.value.contractBindings.length, 1)
assert.equal(s.form.value.contractBindings[0].contractId, '')
await s.save()
assert.equal(h.writes.length, 0)
s.changeBindingContract(0, 'c1')
h.productRequests.at(-1).resolve([{ id: 'p1', productName: '产品1' }])
await Promise.resolve()
await Promise.resolve()
s.setBindingProducts(0, ['p1'])
s.changeBindingContract(0, 'c2')
assert.deepEqual(s.form.value.contractBindings[0].products, [])
assert.equal(s.form.value.contractBindings[0].contractId, 'c2')
} finally {
h.stop()
}
})
test('referenced bindings and references cannot be edited or removed', async () => {
const h = harness(),
s = h.state
try {
h.setCert({
referencesEditable: false,
contractBindings: [
{
id: 'b',
contractId: 'c',
editLocked: true,
products: [{ id: 'p', contractProductId: 'p1' }]
}
],
purchaseRefs: [{ id: 'r', referenceType: 'unknown', referenceNo: 'ref' }]
})
s.kind.value = 'transport'
await s.edit({ id: 'cert' })
s.removeContractBinding(0)
assert.equal(s.form.value.contractBindings.length, 1)
s.changeBindingContract(0, 'other')
assert.equal(s.form.value.contractBindings[0].contractId, 'c')
s.removeReference(0)
s.addReference()
assert.equal(s.form.value.purchaseRefs.length, 1)
assert.ok(h.messages.some((m) => m.includes('联系管理员')))
} finally {
h.stop()
}
})
test('recognition cannot populate locked empty references while other empty fields remain fillable', async () => {
const h = harness(),
s = h.state
try {
h.setCert({ referencesEditable: false, purchaseRefs: [], attachmentUrls: 'one.png' })
s.kind.value = 'transport'
await s.edit({ id: 'cert' })
const parsing = s.recognize()
h.parses[0].resolve({
candidates: {
purchaseRefs: [{ referenceType: 'unknown', referenceNo: 'OCR-NEW' }],
shipperName: '合成托运单位'
},
invalidFields: {}
})
await parsing
assert.deepEqual(s.form.value.purchaseRefs, [])
assert.equal(s.form.value.shipperName, '合成托运单位')
} finally {
h.stop()
}
})
test('successful edit clears attachment and remark and becomes a fresh open form', async () => {
const h = harness(),
s = h.state
try {
await s.edit({ id: 'existing' })
s.form.value.attachmentUrls = 'one.png'
s.form.value.remark = '本张备注'
await s.save()
assert.equal(h.writes[0].body.remark, '本张备注')
assert.equal(s.formVisible.value, true)
assert.equal(s.editingId.value, '')
assert.equal(s.form.value.attachmentUrls, '')
assert.equal(s.form.value.remark, '')
s.form.value.attachmentUrls = 'next.png'
await s.save()
assert.equal(h.writes[1].id, undefined)
} finally {
h.stop()
}
})
test('save and recognition reject missing or historical multiple attachments', async () => {
const h = harness(),
s = h.state
try {
await s.edit({ id: 'existing' })
for (const urls of ['', 'a.png,b.png']) {
s.form.value.attachmentUrls = urls
await vue.nextTick()
s.recognitionImage.value = urls.split(',')[0]
await s.save()
const parsing = s.recognize()
for (const p of h.parses) p.resolve({ candidates: {}, invalidFields: {} })
await parsing
}
assert.equal(h.writes.length, 0)
assert.equal(h.parses.length, 0)
} finally {
h.stop()
}
})
test('new transport save keeps its type open while failures and recognition preserve the current remark', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
await s.changeFormKind('transport')
s.form.value.attachmentUrls = 'one.png'
s.form.value.remark = '不得丢失'
await s.save()
assert.equal(s.formVisible.value, true)
assert.equal(s.kind.value, 'transport')
assert.equal(s.formKind.value, 'transport')
assert.equal(s.form.value.remark, '')
assert.deepEqual(s.form.value.purchaseRefs, [])
assert.deepEqual(s.form.value.contractBindings, [])
s.form.value.attachmentUrls = 'two.png'
s.form.value.remark = '失败保留'
s.recognizing.value = true
await s.save()
assert.equal(h.writes.length, 1)
assert.equal(s.form.value.remark, '失败保留')
s.recognizing.value = false
h.failSave()
await assert.rejects(s.save(), /保存失败/)
assert.equal(s.form.value.remark, '失败保留')
assert.equal(s.form.value.attachmentUrls, 'two.png')
assert.equal(s.formVisible.value, true)
} finally {
h.stop()
}
})
test('saving while transport purchase candidates load refreshes candidates for the next session', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
const old = h.deferOptions()
const switching = s.changeFormKind('transport')
const fresh = h.deferOptions()
s.form.value.attachmentUrls = 'one.png'
await s.save()
old.resolve([{ id: 'stale' }])
await switching
assert.deepEqual(s.purchases.value, [])
fresh.resolve([{ id: 'fresh' }])
await Promise.resolve()
await vue.nextTick()
assert.deepEqual(s.purchases.value, [{ id: 'fresh' }])
assert.equal(s.formVisible.value, true)
} finally {
h.stop()
}
})