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

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

578 lines
19 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { parse } from '@vue/compiler-sfc'
import ts from 'typescript'
import * as vue from 'vue'
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
import {
PL_PLAN_FOLLOW_STATUS_OPTIONS,
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType
} from '../src/utils/pl-display'
const flush = async () => {
for (let i = 0; i < 30; i++) await Promise.resolve()
}
function deferred() {
let resolve!: (value: any) => void, reject!: (error: any) => void
const promise = new Promise((a, b) => {
resolve = a
reject = b
})
return { promise, resolve, reject }
}
function harness(fixed?: string) {
const viewportWidth = vue.ref(1120)
const calls: any[] = [],
permissions: any[] = [],
unmount: (() => void)[] = []
const props = vue.reactive({
contractId: fixed,
active: true,
embedded: !!fixed
})
const user = vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } })
const queue = (method: string, args: any[]) => {
const pending = deferred()
calls.push({ method, args, ...pending })
return pending.promise
}
const api = Object.fromEntries(
['listStops', 'getStops', 'save'].map((method) => [
method,
(...args: any[]) => queue(method, args)
])
)
const env = {
...Object.fromEntries(
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
),
defineProps: () => props,
withDefaults: (v: any) => v,
useUserStore: () => user,
fetchGetMenuCodes: () => {
const p = deferred()
permissions.push(p)
return p.promise
},
useElementSize: () => ({ width: viewportWidth }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
usePersistedListColumns,
useServerColumnFilters,
PL_PLAN_FOLLOW_STATUS_OPTIONS,
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType,
legalDocumentApi: api,
ElMessage: { success() {}, warning() {}, error() {} },
onBeforeUnmount: (fn: () => void) => unmount.push(fn)
}
const src = ts.createSourceFile(
'panel.ts',
parse(
readFileSync(
new URL('../src/views/pl/legal-use-document/StopDocuments.vue', import.meta.url),
'utf8'
)
).descriptor.scriptSetup!.content,
ts.ScriptTarget.Latest,
true
)
const code = ts.transpile(
src.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(src))
.join('\n'),
{ target: ts.ScriptTarget.ESNext }
)
const scope = vue.effectScope()
const state = scope.run(() =>
new Function(
...Object.keys(env),
`${code}; return { rows, details, load, loadDetail, allowed, canUpload, keyword, setFilterState, page, total, expansionStyle, savePlan, onCertDraft, drafts, uploaders, saving, status, search, statusLabel, reset, visibleColumns }`
)(...Object.values(env))
)
return {
state,
viewportWidth,
calls,
permissions,
props,
user,
stop: () => {
unmount.forEach((fn) => fn())
scope.stop()
}
}
}
test('global and fixed contract preserve server counts and scope every request', async () => {
for (const fixed of [undefined, 'contract-one']) {
const h = harness(fixed),
s = h.state
h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list', 'pl:plan-cert:upload'] })
await flush()
assert.equal(h.calls[0].method, 'listStops')
assert.equal(h.calls[0].args[0].contractId, fixed)
assert.equal(h.calls[0].args[0].status, 'pending')
h.calls[0].resolve({
list: [{ id: 'plan', uploadedCount: 1, requiredCount: fixed ? 1 : 3 }],
total: 45
})
await flush()
assert.equal(s.rows.value[0].requiredCount, fixed ? 1 : 3)
s.details.plan = {
loading: false,
error: false,
data: { id: 'plan', planProductIds: ['pp'], legalUseCertUrl: 'a,b', items: [] }
}
s.drafts.plan = 'a,b'
const saving = s.savePlan('plan')
assert.equal(h.calls[1].method, 'save')
assert.deepEqual(h.calls[1].args, ['plan', [{ id: 'pp', legalUseCertUrl: 'a,b' }]])
h.calls[1].resolve({})
await saving
h.stop()
}
})
test('busy uploads refuse saves and failures preserve draft', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 0 })
await flush()
s.details.p = {
loading: false,
error: false,
data: { planProductIds: ['pp'], legalUseCertUrl: 'original' }
}
s.drafts.p = 'draft'
s.uploaders.p = { hasUploading: () => true }
await s.savePlan('p')
assert.equal(h.calls.length, 1)
s.uploaders.p = { hasUploading: () => false }
const saving = s.savePlan('p')
h.calls[1].reject(Error('failed'))
await saving
assert.equal(s.drafts.p, 'draft')
assert.equal(s.saving.p, false)
h.stop()
})
for (const change of ['contract', 'tenant'])
test(`old detail is discarded after ${change}`, async () => {
const h = harness('one'),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 0 })
await flush()
const old = s.loadDetail({ id: 'p' })
if (change === 'contract') h.props.contractId = 'two'
if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b'
h.calls.find((call) => call.method === 'getStops').resolve({
legalUseCertUrl: 'stale',
planProductIds: ['pp']
})
await old
assert.equal(s.drafts.p, undefined)
h.stop()
})
test('search during permission initialization does not cancel it; filters are independent', async () => {
const h = harness(),
s = h.state
s.keyword.value = 'word'
s.setFilterState('planNo', { text: 'PL' })
s.setFilterState('planDate', { from: '2026-11-01', to: '2026-11-04' })
s.setFilterState('licensePlate', { text: '鄂A' })
await s.load()
h.permissions[0].resolve({ all: true })
await flush()
assert.equal(h.calls.length, 1)
assert.equal(h.calls[0].args[0].keyword, 'word')
assert.equal(h.calls[0].args[0].planNo, 'PL')
assert.equal(h.calls[0].args[0].planDateFrom, '2026-11-01')
assert.equal(h.calls[0].args[0].planDateTo, '2026-11-04')
assert.equal(h.calls[0].args[0].licensePlate, '鄂A')
h.stop()
})
test('refresh preserves page and total; not_required is not uploaded', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 45 })
await flush()
s.page.value = 2
const pending = s.load()
assert.equal(s.total.value, 45)
assert.equal(h.calls[1].args[0].page, 2)
h.calls[1].reject(Error('failed'))
await pending
assert.equal(s.page.value, 2)
assert.equal(s.total.value, 45)
assert.equal(s.statusLabel('not_required'), '无需上传')
h.stop()
})
// Mount the real Element Plus pager. Its page-count watcher must remain active.
import ElPagination from 'element-plus/es/components/pagination/src/pagination.mjs'
test('real pagination keeps page two while loading and preserves count after request failure', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [{ id: 'first' }], total: 45 })
await flush()
const renderer = vue.createRenderer<any, any>({
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText: () => {},
setElementText: () => {},
parentNode: () => null,
nextSibling: () => null,
insert: () => {},
remove: () => {},
patchProp: () => {}
})
const app = renderer.createApp({
render: () =>
vue.h(ElPagination, {
currentPage: s.page.value,
total: s.total.value,
pageSize: 20,
layout: '',
'onUpdate:current-page': (value: number) => {
s.page.value = value
},
onCurrentChange: () => {
void s.load()
}
})
})
app.mount({})
s.page.value = 2
const pending = s.load()
await vue.nextTick()
await flush()
assert.equal(s.page.value, 2)
assert.equal(h.calls.length, 2)
assert.equal(h.calls[1].args[0].page, 2)
h.calls[1].resolve({ list: [{ id: 'second' }], total: 45 })
await pending
await vue.nextTick()
assert.equal(s.page.value, 2)
assert.equal(s.rows.value[0].id, 'second')
const failed = s.load()
h.calls[2].reject(Error('failed'))
await failed
await vue.nextTick()
assert.equal(s.page.value, 2)
assert.equal(s.total.value, 45)
assert.deepEqual(s.rows.value, [])
h.user.getUserInfo.tenantNo = 'next'
await vue.nextTick()
assert.equal(s.page.value, 1)
assert.equal(s.total.value, 0)
app.unmount()
h.stop()
})
import { compileScript } from '@vue/compiler-sfc'
function compiledSfc(path: string, dependencies: Record<string, any>, inlineTemplate = true) {
const descriptor = parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor
const compiled = compileScript(descriptor, { id: path, inlineTemplate })
const source = ts.createSourceFile('compiled.ts', compiled.content, ts.ScriptTarget.Latest, true)
const env: Record<string, any> = Object.fromEntries(
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
)
Object.assign(env, dependencies)
for (const statement of source.statements) {
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly) continue
const clause = statement.importClause
if (clause?.name) env[clause.name.text] = dependencies[clause.name.text] || {}
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
for (const item of clause.namedBindings.elements) {
if (item.isTypeOnly) continue
const name = (item.propertyName || item.name).text
env[item.name.text] =
statement.moduleSpecifier.getText(source) === '"vue"' ||
statement.moduleSpecifier.getText(source) === "'vue'"
? (vue as any)[name]
: dependencies[name]
}
}
}
const code = ts.transpile(
source.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(source))
.join('\n')
.replace('export default', 'const component ='),
{ target: ts.ScriptTarget.ESNext }
)
return new Function(...Object.keys(env), `${code}; return component`)(...Object.values(env))
}
for (const active of [undefined, false])
test(`compiled proof component active=${active}`, async () => {
const calls: string[] = []
const component = compiledSfc(
'../src/views/pl/legal-use-document/StopDocuments.vue',
{
useUserStore: () => vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } }),
fetchGetMenuCodes: async () => ({ menuCodes: ['pl:legal-use-document:list'] }),
legalDocumentApi: {
listStops: async () => {
calls.push('proof')
return { list: [], total: 0 }
}
},
usePersistedListColumns,
useServerColumnFilters,
PL_PLAN_FOLLOW_STATUS_OPTIONS,
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType,
useElementSize: () => ({ width: vue.ref(1000) }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) })
},
false
)
component.render = () => null
const renderer = vue.createRenderer<any, any>({
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText() {},
setElementText() {},
parentNode: () => null,
nextSibling: () => null,
insert() {},
remove() {},
patchProp() {}
})
const app = renderer.createApp(component, active === undefined ? {} : { active })
app.mount({})
await flush()
assert.deepEqual(calls, active === false ? [] : ['proof'])
app.unmount()
})
test('a newer detail load is not cleared when the previous request finishes', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 0 })
await flush()
const first = s.loadDetail({ id: 'p' })
const second = s.loadDetail({ id: 'p' })
h.calls[1].resolve({ legalUseCertUrl: 'old', planProductIds: ['old'] })
await first
h.calls[2].resolve({ legalUseCertUrl: 'new', planProductIds: ['new'] })
await second
assert.equal(s.drafts.p, 'new')
assert.deepEqual(s.details.p.data.planProductIds, ['new'])
h.stop()
})
test('plan certificate is edited below the segments and the expansion stays aligned', () => {
const descriptor = parse(
readFileSync(
new URL('../src/views/pl/legal-use-document/StopDocuments.vue', import.meta.url),
'utf8'
)
).descriptor
assert.match(descriptor.template!.content, /合法使用证明/)
assert.doesNotMatch(descriptor.template!.content, /历史计划级资料|维护本段资料/)
assert.match(descriptor.template!.content, /v-model="drafts\[row\.id\]"/)
assert.match(descriptor.styles[0].content, /text-align: left/)
const h = harness()
assert.equal(h.state.expansionStyle.value.width, '1120px')
h.viewportWidth.value = 760
assert.equal(h.state.expansionStyle.value.width, '760px')
h.stop()
})
test('stop API preserves original legacy methods and scopes all three new endpoints', async () => {
const calls: any[] = []
const source = ts.createSourceFile(
'api.ts',
readFileSync(new URL('../src/api/pl/quota-cert.ts', import.meta.url), 'utf8'),
ts.ScriptTarget.Latest,
true
)
const code = ts.transpile(
source.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(source).replace(/^export /, ''))
.join('\n'),
{ target: ts.ScriptTarget.ESNext }
)
const api = new Function('request', 'pickBxData', `${code};return legalDocumentApi`)(
{
get: (...args: any[]) => {
calls.push(args)
return Promise.resolve({})
},
patch: (...args: any[]) => {
calls.push(args)
return Promise.resolve({})
}
},
(v: any) => v
)
await api.listStops({ contractId: 'c', page: 2, status: 'pending', planNo: 'PL' })
await api.getStops('p', 'c')
await api.saveStops(
'p',
[{ stopType: 'delivery', stopProductId: 'd', legalUseCertUrl: 'a,b' }],
'c'
)
assert.deepEqual(calls, [
[
'/transport/legal-use-documents/stops',
{ params: { contractId: 'c', page: 2, status: 'pending', planNo: 'PL' } }
],
['/transport/legal-use-documents/stops/p', { params: { contractId: 'c' } }],
[
'/transport/legal-use-documents/stops/p',
{ items: [{ stopType: 'delivery', stopProductId: 'd', legalUseCertUrl: 'a,b' }] },
{ params: { contractId: 'c' } }
]
])
for (const method of ['list', 'get', 'save']) assert.equal(typeof api[method], 'function')
})
test('view permission loads proof only and prevents upload requests', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list'] })
await flush()
assert.equal(s.allowed.value, true)
assert.equal(s.canUpload.value, false)
await s.savePlan('p')
assert.deepEqual(
h.calls.map((c) => c.method),
['listStops']
)
h.stop()
})
test('an older save cannot replace a newer draft', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 0 })
await flush()
s.details.p = {
loading: false,
error: false,
data: { planProductIds: ['pp'], legalUseCertUrl: 'a' }
}
s.drafts.p = 'a'
const oldSave = s.savePlan('p')
s.drafts.p = 'b'
h.calls[1].resolve({})
await oldSave
assert.equal(s.drafts.p, 'b')
assert.equal(h.calls.filter((call) => call.method === 'save').length, 1)
assert.equal(s.saving.p, false)
h.stop()
})
test('old list and permission responses cannot reauthorize another tenant', async () => {
const h = harness(),
s = h.state
h.user.getUserInfo.tenantNo = 'b'
h.permissions[0].resolve({ all: true })
await flush()
assert.equal(h.calls.length, 0)
h.permissions[1].resolve({ all: true })
await flush()
h.user.getUserInfo.tenantNo = 'c'
h.calls[0].resolve({ list: [{ id: 'stale' }], total: 1 })
await flush()
assert.deepEqual(s.rows.value, [])
assert.equal(s.allowed.value, false)
h.permissions[2].resolve({ menuCodes: [] })
await flush()
assert.equal(h.calls.length, 1)
h.stop()
})
test('status is a server single filter and combines with the number filter', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
s.setFilterState('documentStatus', { text: '', selected: ['uploaded'] })
assert.equal(h.calls.at(-1).args[0].status, 'uploaded')
s.setFilterState('planNo', { text: 'PL' })
assert.equal(h.calls.at(-1).args[0].planNo, 'PL')
assert.equal(h.calls.at(-1).args[0].status, 'uploaded')
h.calls.forEach((c) => c.resolve({ list: [], total: 0 }))
await flush()
h.stop()
})
test('latest refresh wins and hidden column/reset preserve server filter semantics', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
const fresh = s.load()
h.calls[1].resolve({ list: [{ id: 'new' }], total: 45 })
await fresh
h.calls[0].resolve({ list: [{ id: 'old' }], total: 1 })
await flush()
assert.equal(s.rows.value[0].id, 'new')
assert.equal(s.total.value, 45)
s.keyword.value = 'quick'
s.page.value = 2
s.setFilterState('planNo', { text: 'PL' })
assert.equal(s.page.value, 1)
assert.equal(h.calls.at(-1).args[0].planNo, 'PL')
s.visibleColumns.value = s.visibleColumns.value.filter((key: string) => key !== 'planNo')
await vue.nextTick()
assert.equal(h.calls.at(-1).args[0].planNo, undefined)
assert.equal(h.calls.at(-1).args[0].status, 'pending')
s.reset()
assert.equal(h.calls.at(-1).args[0].keyword, '')
assert.equal(h.calls.at(-1).args[0].status, 'pending')
h.calls.forEach((c) => c.resolve({ list: [], total: 0 }))
await flush()
h.stop()
})
test('list certificate column saves the finished attachment without a second click', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list', 'pl:plan-cert:upload'] })
await flush()
h.calls[0].resolve({ list: [], total: 0 })
await flush()
const row = { id: 'plan', legalUseCertUrl: 'old', planProductIds: ['pp'], documentStatus: 'pending' }
s.onCertDraft(row, 'old')
assert.equal(h.calls.length, 1)
s.onCertDraft(row, 'new')
assert.equal(h.calls[1].method, 'save')
assert.deepEqual(h.calls[1].args, ['plan', [{ id: 'pp', legalUseCertUrl: 'new' }]])
h.calls[1].resolve({})
await flush()
assert.equal(h.calls[2].method, 'listStops')
h.calls[2].resolve({ list: [], total: 0 })
await flush()
h.stop()
})
test('expanded segment columns prioritize upload state and attachments', () => {
const template = parse(
readFileSync(
new URL('../src/views/pl/legal-use-document/StopDocuments.vue', import.meta.url),
'utf8'
)
).descriptor.template!.content
const table = template.slice(template.indexOf('<el-table\n'), template.indexOf('</el-table>'))
assert.deepEqual(
[...table.matchAll(/<el-table-column\b[^>]*\blabel="([^"]+)"/g)].map((match) => match[1]),
['运输段', '产品', '合同编号', '本段主体', '规格', '单位', '本段位置']
)
})