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({ 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, 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 = 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({ 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('')) assert.deepEqual( [...table.matchAll(/]*\blabel="([^"]+)"/g)].map((match) => match[1]), ['运输段', '产品', '合同编号', '本段主体', '规格', '单位', '本段位置'] ) })