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' 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', 'saveStops'].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, legalDocumentApi: api, ElMessage: { success() {}, warning() {} }, 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, open, save, visible, editing, upload, saving, editorLoading, 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) 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) const pending = s.open({ id: 'plan' }, { stopType: 'delivery', stopProductId: 'd2' }) assert.equal(h.calls[1].args[1], fixed) h.calls[1].resolve({ id: 'plan', items: [ { stopType: 'delivery', stopProductId: 'd2', productId: 'same', legalUseCertUrl: 'a,b' } ] }) await pending const saving = s.save() assert.deepEqual(h.calls[2].args, [ 'plan', [{ stopType: 'delivery', stopProductId: 'd2', legalUseCertUrl: 'a,b' }], fixed ]) h.calls[2].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() const opened = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' }) h.calls[1].resolve({ id: 'p', items: [{ stopType: 'load', stopProductId: 'l', legalUseCertUrl: 'original' }] }) await opened s.editing.value.legalUseCertUrl = 'draft' s.upload.value = { hasUploading: () => true } await s.save() assert.equal(h.calls.length, 2) s.upload.value = { hasUploading: () => false } const saving = s.save() h.calls[2].reject(Error('failed')) await saving assert.equal(s.editing.value.legalUseCertUrl, 'draft') assert.equal(s.visible.value, true) h.stop() }) for (const change of ['contract', 'tenant', 'close']) test(`old editor response is discarded after ${change}`, async () => { const h = harness('one'), s = h.state h.permissions[0].resolve({ all: true }) await flush() const old = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' }) if (change === 'contract') h.props.contractId = 'two' if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b' if (change === 'close') s.visible.value = false h.calls[1].resolve({ id: 'p', items: [{ stopType: 'load', stopProductId: 'l' }] }) await old assert.equal(s.editing.value, 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' }) 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') 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, 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('closing and reopening rejects old response and finally without clearing new loading', async () => { const h = harness(), s = h.state h.permissions[0].resolve({ all: true }) await flush() const first = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' }) s.visible.value = false const second = s.open({ id: 'p' }, { stopType: 'delivery', stopProductId: 'd' }) h.calls[1].resolve({ items: [{ stopType: 'load', stopProductId: 'l' }] }) await first assert.equal(s.editorLoading.value, true) assert.equal(s.editing.value, undefined) h.calls[2].resolve({ items: [{ stopType: 'delivery', stopProductId: 'd' }] }) await second assert.equal(s.editorLoading.value, false) assert.equal(s.editing.value.stopProductId, 'd') h.stop() }) test('historical attachments have a separate readonly field and viewport aligned expansion', () => { 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.match( descriptor.template!.content, /:model-value="product.legalUseCertUrl \|\| ''"\s+disabled/ ) 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.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' }) await s.save() assert.deepEqual( h.calls.map((c) => c.method), ['listStops'] ) h.stop() }) test('old save completion cannot close or clear a reopened editor', async () => { const h = harness(), s = h.state h.permissions[0].resolve({ all: true }) await flush() const opened = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' }) h.calls[1].resolve({ items: [{ stopType: 'load', stopProductId: 'l', legalUseCertUrl: 'a' }] }) await opened const oldSave = s.save() s.visible.value = false const reopened = s.open({ id: 'p' }, { stopType: 'delivery', stopProductId: 'd' }) h.calls[3].resolve({ items: [{ stopType: 'delivery', stopProductId: 'd', legalUseCertUrl: 'b' }] }) await reopened const newSave = s.save() h.calls[2].resolve({}) await oldSave assert.equal(s.visible.value, true) assert.equal(s.saving.value, true) assert.equal(s.editing.value.stopProductId, 'd') assert.equal(h.calls.length, 5) h.calls[4].reject(Error('retry')) await newSave assert.equal(s.editing.value.legalUseCertUrl, 'b') assert.equal(s.saving.value, 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.status.value = 'uploaded' s.search() 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.status.value = 'pending' 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, undefined) h.calls.forEach((c) => c.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]), [ '运输段', '产品', '上传情况', '附件', '合同编号', '本段主体', '规格', '单位', '本段位置', '上传时间', '操作' ] ) })