Files
shanxia2.0/web-admin/scripts/quota-cert-form.test.ts
T
wuzilongandCursor 8e9536332a feat: 保存运输证件、报销付款和开发流程的当前实现。
补齐证书齐备、证件折算、移动用证和合同绑定,并带上报销批量发放、付款备注以及个人开发流程升级,便于继续讨论已安排计划的调整规则。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 21:42:33 +08:00

898 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
import { getProductUnitLabel } from '../src/utils/crm-product-display'
import { buyPartyName, expiryWarning, salePartyName } from '../src/views/pl/quota-cert/cert-list-face'
import {
bindingsFromPurchaseCerts,
matchPurchaseCert
} from '../src/views/pl/quota-cert/contract-binder-rules'
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: {} }),
useRouter: () => ({ push() {}, replace() {} }),
useUserStore: () => user,
useDictStore: () => ({
getLabel: (_type: string, value: string) => (value === 'dun' ? '吨' : value || '—'),
getOptions: () => [{ value: 'dun', label: '吨', tagType: '' }]
}),
getProductUnitLabel,
buyPartyName,
expiryWarning,
salePartyName,
bindingsFromPurchaseCerts,
matchPurchaseCert,
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('each contract link keeps its own tab, including repeated contracts', () => {
const h = harness()
try {
const tabs = h.state.contractTabItems(
[
{ id: 'link-1', contractId: 'c1', products: [] },
{ id: 'link-2', contractId: 'c1', products: [] },
{ id: 'link-3', contractId: 'c2', products: [] }
],
[
{ id: 'c1', contractNo: 'HT-1', contractName: '重复合同' },
{ id: 'c2', contractNo: 'HT-2', contractName: '另一合同' }
]
)
assert.deepEqual(
tabs.map((tab: { key: string; label: string }) => [tab.key, tab.label]),
[
['link-1', 'HT-1(1)'],
['link-2', 'HT-1(2)'],
['link-3', 'HT-2']
]
)
} finally {
h.stop()
}
})
test('replacing certificate products keeps the other certificates on the contract', () => {
const result = model.replaceCertProducts(
[
{ certType: 'transport', certId: 't', products: [{ contractProductId: 'keep' }] },
{
certType: 'purchase',
certId: 'p',
id: 'link',
products: [{ id: 'old', contractProductId: 'a' }]
}
],
'purchase',
'p',
[{ contractProductId: 'b' }]
)
assert.equal(result[0].certId, 't')
assert.deepEqual(result[0].products, [{ contractProductId: 'keep' }])
assert.equal(result[1].id, 'link')
assert.deepEqual(result[1].products, [{ contractProductId: 'b' }])
})
test('certificate quantities display three decimal places', () => {
assert.equal(model.formatCertQty('1700.000000'), '1700.000')
assert.equal(model.formatCertQty('0.000000'), '0.000')
assert.equal(model.formatCertQty('1.23456'), '1.235')
assert.equal(model.formatCertQty('9.9995'), '10.000')
assert.equal(model.formatCertQty('-1.2345'), '-1.235')
assert.equal(model.formatCertQty('-0.0004'), '0.000')
assert.equal(model.formatCertQty(''), '—')
assert.equal(model.formatCertQty(null), '—')
})
test('new certificate defaults to review after image parsing', () => {
assert.equal(model.newCert('purchase').entryMode, 'image_parse')
assert.equal(model.newCert('transport').entryMode, 'image_parse')
})
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('unconfigured image parsing warns and keeps the manual entry', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.certNo = '手工证号'
s.form.value.attachmentUrls = 'one.png'
await vue.nextTick()
const parsing = s.recognize()
h.parses[0].reject(new Error('图片解析尚未配置,可继续手工录入'))
await parsing
assert.equal(s.form.value.certNo, '手工证号')
assert.equal(s.recognizing.value, false)
assert.ok(h.messages.includes('图片解析尚未配置,可继续手工录入'))
} 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('确定产品时保留原范围身份,已用证不能增删产品', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
s.form.value.attachmentUrls = 'one.png'
s.addContractBinding('c')
s.form.value.contractBindings[0].products = [
{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }
]
s.acceptFormBinding({
contractId: 'c',
contractNo: 'HT',
contractName: '合同',
products: [
{ contractProductId: 'p', scopeStatus: 'inactive' },
{ contractProductId: 'p2', scopeStatus: 'active' }
]
})
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
assert.equal(s.form.value.contractBindings[0].products[0].scopeStatus, 'inactive')
assert.equal(s.form.value.contractBindings[0].products[1].contractProductId, 'p2')
s.form.value.contractBindings[0].editLocked = true
s.acceptFormBinding({
contractId: 'c',
contractNo: 'HT',
contractName: '合同',
products: [{ contractProductId: 'p2', scopeStatus: 'active' }]
})
assert.equal(s.form.value.contractBindings[0].products.length, 2)
assert.ok(h.messages.some((message: string) => message.includes('不能增删产品')))
s.acceptFormBinding({
contractId: 'c',
contractNo: 'HT',
contractName: '合同',
products: [
{ contractProductId: 'p', scopeStatus: 'active' },
{ contractProductId: 'p2', scopeStatus: 'inactive' }
]
})
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
assert.equal(s.form.value.contractBindings[0].products[0].scopeStatus, 'active')
} 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 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.acceptFormBinding({
contractId: 'one',
contractNo: 'ONE',
contractName: '合同一',
products: [{ contractProductId: 'p1', scopeStatus: 'active' }]
})
s.acceptFormBinding({
contractId: 'two',
contractNo: 'TWO',
contractName: '合同二',
products: [{ contractProductId: 'p2', scopeStatus: 'active' }]
})
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()
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.acceptFormBinding({
contractId: 'c2',
contractNo: 'HT-2',
contractName: '另一合同',
products: [{ contractProductId: 'p2', scopeStatus: 'active' }]
})
assert.equal(s.form.value.contractBindings[1].contractId, 'c2')
assert.equal(s.form.value.contractBindings[1].products[0].contractProductId, 'p2')
assert.equal(s.form.value.contractBindings[0].contractId, '')
} 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.acceptFormBinding({
contractId: 'c',
contractNo: 'HT',
contractName: '合同',
products: [{ contractProductId: 'other', scopeStatus: 'active' }]
})
assert.equal(s.form.value.contractBindings[0].products[0].contractProductId, 'p1')
s.removeReference(0)
s.addReference()
assert.equal(s.form.value.purchaseRefs.length, 1)
assert.ok(h.messages.some((m) => m.includes('不能新增或移除证面引用')))
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[0]), false)
} finally {
h.stop()
}
})
test('linked purchase archive stays locked while an empty archive can be completed', async () => {
const h = harness(),
s = h.state
try {
h.setCert({
referencesEditable: false,
purchaseRefs: [
{ id: 'open', referenceType: 'unknown', referenceNo: 'G35', purchaseCertId: null },
{ id: 'linked', referenceType: 'purchase_permit', referenceNo: 'G1', purchaseCertId: 'p1' }
]
})
s.kind.value = 'transport'
await s.edit({ id: 'cert' })
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[0]), false)
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[1]), true)
s.purchases.value = [{ id: 'buy', certNo: 'G34260485131004', certType: 'purchase' }]
s.form.value.purchaseRefs[0].purchaseCertId = 'buy'
s.onPurchaseArchiveChange()
assert.equal(s.form.value.purchaseRefs[0].referenceNo, 'G35')
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[0]), false)
} 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()
}
})
test('transport recognition copies contracts and products from the matched purchase cert', async () => {
const h = harness(),
s = h.state
try {
await s.edit()
await s.changeFormKind('transport')
s.form.value.attachmentUrls = 'cert.pdf'
await vue.nextTick()
h.setCert({
contractBindings: [
{
id: 'purchase-link',
contractId: 'c1',
linkStatus: 'active',
products: [{ id: 'purchase-scope', contractProductId: 'p1', scopeStatus: 'active' }]
}
]
})
const options = h.deferOptions()
const parsing = s.recognize()
h.parses[0].resolve({
candidates: {
purchaseRefs: [{ referenceType: 'unknown', referenceNo: 'G34260671919592' }]
},
invalidFields: {}
})
options.resolve([{ id: 'buy-1', certNo: 'G34260671919592' }])
await parsing
assert.equal(s.form.value.purchaseRefs[0].purchaseCertId, 'buy-1')
assert.equal(s.form.value.contractBindings.length, 1)
assert.equal(s.form.value.contractBindings[0].contractId, 'c1')
assert.equal(s.form.value.contractBindings[0].products[0].contractProductId, 'p1')
assert.equal(s.form.value.contractBindings[0].products[0].id, undefined)
assert.equal(
s.bindingContracts.value.some((contract: { id: string }) => contract.id === 'old'),
true
)
} finally {
h.stop()
}
})
test('deleting the certificate attachment clears face, bindings and references', () => {
const h = harness(),
s = h.state
try {
s.formKind.value = 'transport'
s.form.value = {
...model.newCert('transport'),
entryMode: 'manual',
attachmentUrls: 'one.png',
remark: '保留备注',
orderNum: 3,
status: 'active',
certNo: 'Y1',
productNameOnCert: '硫酸',
totalQty: '10',
vehicleNumbersJson: ['皖A1'],
purchaseRefs: [{ referenceNo: 'G1' }],
contractBindings: [{ contractId: 'c1', linkStatus: 'active', products: [{ id: 'p1' }] }],
bindingsEditable: true,
referencesEditable: false
}
s.edited.add('certNo')
s.bindingContracts.value = [{ id: 'c1', contractNo: 'HT-1' }]
s.lockedPlates.value = ['皖A1']
s.linkedRefIds.value = ['ref-1']
s.onCertificateAttachmentChange('one.png')
assert.equal(s.form.value.certNo, 'Y1')
assert.equal(s.bindingEpoch.value, 0)
s.onCertificateAttachmentChange('')
assert.equal(s.form.value.certNo, '')
assert.equal(s.form.value.productNameOnCert, '')
assert.equal(s.form.value.totalQty, '')
assert.deepEqual(s.form.value.vehicleNumbersJson, [])
assert.deepEqual(s.form.value.purchaseRefs, [])
assert.deepEqual(s.form.value.contractBindings, [])
assert.equal(s.form.value.remark, '保留备注')
assert.equal(s.form.value.entryMode, 'manual')
assert.equal(s.form.value.orderNum, 3)
assert.equal(s.form.value.status, 'active')
assert.equal(s.form.value.attachmentUrls, '')
assert.equal(s.form.value.referencesEditable, false)
assert.equal(s.edited.has('certNo'), false)
assert.deepEqual(s.bindingContracts.value, [])
assert.deepEqual(s.lockedPlates.value, [])
assert.deepEqual(s.linkedRefIds.value, [])
assert.equal(s.bindingEpoch.value, 1)
const archive = readFileSync(
new URL('../src/views/m/pl/plan-cert/archive-form.vue', import.meta.url),
'utf8'
)
assert.match(archive, /onCertificateAttachmentChange/)
assert.match(archive, /clearCertAfterAttachmentRemoved/)
} finally {
h.stop()
}
})