import test from 'node:test' import assert from 'node:assert/strict' import { createStatisticsLoader } from '../src/views/pl/quota-statistics/loader' test('fixed contract skips global list and retains each decimal balance', async () => { const calls: string[] = [] const loader = createStatisticsLoader({ contracts: async () => { calls.push('global') }, contract: async (id: string) => { calls.push(id) return { certificates: [{ availableQty: '0.10000001' }, { availableQty: '7.2' }] } } } as any) const result = await loader.load({ mode: 'contract', contractId: 'c1', allowed: true }) assert.deepEqual(calls, ['c1']) assert.deepEqual( result.certificates.map((c: any) => c.availableQty), ['0.10000001', '7.2'] ) }) test('permission denied never requests statistics', async () => { const loader = createStatisticsLoader( new Proxy( {}, { get: () => () => { throw Error('unexpected request') } } ) as any ) assert.equal(await loader.load({ mode: 'contract', allowed: false }), undefined) }) test('context invalidation discards pending response', async () => { let resolve!: (v: any) => void const loader = createStatisticsLoader({ contract: () => new Promise((r) => { resolve = r }) } as any) const pending = loader.load({ mode: 'contract', contractId: 'old', allowed: true }) loader.invalidate() resolve({ id: 'old' }) assert.equal(await pending, undefined) }) import { readFileSync } from 'node:fs' import { parse } from '@vue/compiler-sfc' import ts from 'typescript' import * as vue from 'vue' import { plRoutes } from '../src/router/modules/pl' 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 calls: any[] = [], permissions: any[] = [], unmount: Function[] = [] const props = vue.reactive({ mode: 'contract', 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( ['contracts', 'contract', 'certificates', 'certificate'].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 }, useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }), usePersistedListColumns, useServerColumnFilters, createStatisticsLoader, quotaStatisticsApi: api, onBeforeUnmount: (fn: Function) => unmount.push(fn) } const src = ts.createSourceFile( 'panel.ts', parse( readFileSync( new URL('../src/views/pl/quota-statistics/StatisticsPanel.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, fixedDetail, details, load, loadDetail, allowed, canEdit, keyword, setFilterState }` )(...Object.values(env)) ) return { state, calls, permissions, props, user, stop: () => { unmount.forEach((fn) => fn()) scope.stop() } } } test('five leaves retain IDs and individual permissions', () => { const group = plRoutes[0].children!.find((r) => r.id === 'PL_GROUP_CERT')! assert.deepEqual( group.children!.map((r) => r.path), ['quota-cert', 'contract-quota', 'certificate-usage', 'plan-cert', 'legal-use-document'] ) assert.equal(group.children![0].id, 'PL_QUOTA_CERT') assert.equal(group.children![3].id, 'PL00000000000000000000000000015') assert.deepEqual( group.children!.map((r) => r.meta!.menuCode), [ 'pl:quota-cert:list', 'pl:quota-cert:list', 'pl:quota-cert:list', 'pl:plan-cert:list', 'pl:legal-use-document:list' ] ) }) test('actual fixed panel loads only current contract and drops old response', async () => { const h = harness('one') h.permissions[0].resolve({ menuCodes: ['pl:quota-cert:list'] }) await flush() assert.equal(h.calls[0].method, 'contract') assert.deepEqual(h.calls[0].args, ['one']) h.props.contractId = 'two' h.permissions[1].resolve({ all: true }) await flush() h.calls[1].resolve({ id: 'two', certificates: [] }) await flush() h.calls[0].resolve({ id: 'one', certificates: [] }) await flush() assert.equal(h.state.fixedDetail.value.id, 'two') h.stop() }) for (const change of ['tenant', 'logout', 'unmount']) test(`actual panel invalidates data on ${change}`, async () => { const h = harness() h.permissions[0].resolve({ all: true }) await flush() if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b' if (change === 'logout') h.user.accessToken = '' if (change === 'unmount') h.stop() h.calls[0].resolve({ list: [{ id: 'stale' }], total: 1 }) await flush() assert.deepEqual(h.state.rows.value, []) assert.equal(h.state.allowed.value, false) h.stop() }) test('proof-only permission cannot request quota', async () => { const h = harness() h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list'] }) await flush() assert.equal(h.calls.length, 0) h.stop() }) test('expand failure clears stale data and retry works', async () => { const h = harness() h.permissions[0].resolve({ all: true }) await flush() h.calls[0].resolve({ list: [{ id: 'c' }], total: 1 }) await flush() const first = h.state.loadDetail({ id: 'c' }) h.calls[1].resolve({ id: 'c', certificates: [] }) await first assert.equal(h.state.details.c.data.id, 'c') const failure = h.state.loadDetail({ id: 'c' }) h.calls[2].reject(Error('failed')) await failure assert.equal(h.state.details.c.data, undefined) assert.equal(h.state.details.c.error, true) const retry = h.state.loadDetail({ id: 'c' }) h.calls[3].resolve({ id: 'c', certificates: [] }) await retry assert.equal(h.state.details.c.data.id, 'c') h.stop() }) test('statistics API maps each view to the server endpoint and preserves response decimals', async () => { const calls: any[] = [] const raw = { list: [{ availableQty: '1.00000001' }], total: 1 } 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 quotaStatisticsApi`)( { get: (url: string, options: any) => { calls.push({ url, options }) return Promise.resolve(raw) } }, (v: any) => v ) assert.equal( (await api.contracts({ page: 2, keyword: '合同' })).list[0].availableQty, '1.00000001' ) await api.certificates({ certType: 'transport', pageSize: 20 }) await api.contract('c') await api.certificate('purchase', 'p') assert.deepEqual( calls.map((c) => c.url), [ '/transport/quota-certs/statistics/contracts', '/transport/quota-certs/statistics/certificates', '/transport/quota-certs/contracts/c/statistics', '/transport/quota-certs/purchase/p/statistics' ] ) assert.deepEqual(calls[1].options.params, { certType: 'transport', pageSize: 20 }) }) import * as statisticsLabels from '../src/views/pl/quota-statistics/labels' test('contract type tags translate all server values and legacy framework', () => { const template = parse( readFileSync( new URL('../src/views/pl/quota-statistics/StatisticsPanel.vue', import.meta.url), 'utf8' ) ).descriptor.template!.content const expression = template.match( /v-else-if="column.prop === 'contractType'"[^>]*>\s*\{\{([\s\S]*?)\}\}/ )![1] const renderLabel = new Function( 'row', ...Object.keys(statisticsLabels), `return (${expression})` ) for (const [contractType, label] of Object.entries({ sale: '销售合同', purchase: '采购合同', framework_sale: '框架销售合同', framework_purchase: '框架采购合同', framework: '框架合同', '': '—' })) { assert.equal(renderLabel({ contractType }, ...Object.values(statisticsLabels)), label) } }) test('usage trace renders business columns and resolves contract/product by their exact identity', () => { const sfc = parse( readFileSync( new URL('../src/views/pl/quota-statistics/StatisticsDetail.vue', import.meta.url), 'utf8' ) ).descriptor const source = ts.createSourceFile( 'detail.ts', sfc.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const code = ts.transpile( source.statements .filter((s) => !ts.isImportDeclaration(s)) .map((s) => s.getText(source)) .join('\n'), { target: ts.ScriptTarget.ESNext } ) const detail = { contracts: [ { id: 'other-contract', contractNo: '不匹配合同', products: [{ contractProductId: 'product-id', model: '不匹配规格' }] }, { id: 'contract-id', contractNo: 'HT-2026-01', products: [{ contractProductId: 'product-id', model: '98%' }] } ], usageItems: [ { id: 'usage-id', contractId: 'contract-id', contractProductId: 'product-id', stopId: 'stop-id', stopProductId: 'stop-product-id', planNo: 'PL-001', stopType: 'delivery', productName: '硫酸', partyName: '本段客户', address: '本段地址' }, { id: 'missing', contractId: 'missing', contractProductId: 'product-id', stopType: 'load' } ] } const rows = new Function( 'defineProps', 'computed', `${code}; return typeof usageRows === 'undefined' ? undefined : usageRows.value` )(() => ({ mode: 'certificate', detail }), vue.computed) assert.ok(rows, '追踪表应提供可读的业务列') assert.deepEqual( rows.map((r: any) => [r.contractNo, r.productModel, r.partyName, r.address]), [ ['HT-2026-01', '98%', '本段客户', '本段地址'], ['—', '—', '—', '—'] ] ) const table = sfc.template!.content.match(//)![0] const props = [...table.matchAll(/ m[1]) assert.ok(['contractNo', 'productModel', 'partyName', 'address'].every((p) => props.includes(p))) assert.ok(!props.some((p) => /Id$/.test(p))) assert.ok(!table.includes('row.stopId')) const stopExpression = table.match(/\s*\{\{([\s\S]*?)\}\}/)![1] assert.equal(new Function('row', `return (${stopExpression})`)(rows[0]), '送货') }) test('quick search and number header filter are sent as independent conditions', async () => { const h = harness() h.permissions[0].resolve({ all: true }) await flush() h.calls[0].resolve({ list: [], total: 0 }) await flush() h.state.keyword.value = '目标合同' h.state.setFilterState('contractNo', { text: 'HT-2026' }) assert.equal(h.calls[1].args[0].keyword, '目标合同') assert.equal(h.calls[1].args[0].contractNo, 'HT-2026') h.calls[1].resolve({ list: [], total: 0 }) await flush() h.stop() }) import { bindingPayload } from '../src/views/pl/quota-cert/model' function scopeDialogHarness() { const calls: any[] = [], unmount: Function[] = [] const queue = (method: string, args: any[]) => { const p = deferred() calls.push({ method, args, ...p }) return p.promise } const api = Object.fromEntries( ['list', 'contracts', 'products', 'bindings', 'saveBindings'].map((method) => [ method, (...args: any[]) => queue(method, args) ]) ) const source = ts.createSourceFile( 'scope.ts', parse( readFileSync( new URL('../src/views/pl/quota-cert/ContractScopeDialog.vue', import.meta.url), 'utf8' ) ).descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const code = ts.transpile( source.statements .filter((s) => !ts.isImportDeclaration(s)) .map((s) => s.getText(source)) .join('\n'), { target: ts.ScriptTarget.ESNext } ) const env = { ...Object.fromEntries( Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default') ), defineProps: () => ({ canEdit: true, fixedContractId: 'c1' }), defineEmits: () => () => {}, defineExpose: () => {}, onBeforeUnmount: (fn: Function) => unmount.push(fn), quotaCertApi: api, quotaStatisticsApi: { contract: (...args: any[]) => queue('contract', args) }, bindingPayload, ElMessage: { success: () => {} } } const scope = vue.effectScope() const state = scope.run(() => new Function( ...Object.keys(env), `${code};return {open, visible, loading, searching, loadedContractId, bindings, save, searchContracts, contracts}` )(...Object.values(env)) ) return { state, calls, stop() { unmount.forEach((fn) => fn()) scope.stop() } } } test('closing and reopening same contract rejects old scope response and keeps save disabled', async () => { const h = scopeDialogHarness(), s = h.state const first = s.open('c1') h.calls[0].resolve([]) h.calls[1].resolve([]) h.calls[2].resolve({ id: 'c1', contractNo: 'HT-1', contractName: '合同' }) await flush() assert.deepEqual( h.calls.slice(3).map((c) => c.method), ['products', 'bindings'] ) s.visible.value = false const second = s.open('c1') h.calls[3].resolve([{ id: 'old-product' }]) h.calls[4].resolve([{ id: 'old-binding', certType: 'purchase', certId: 'p', products: [] }]) await first assert.equal(s.loadedContractId.value, '') assert.equal(s.loading.value, true) assert.deepEqual(s.bindings.value, []) const count = h.calls.length await s.save() assert.equal(h.calls.length, count) h.calls[5].resolve([]) h.calls[6].resolve([]) h.calls[7].resolve({ id: 'c1', contractNo: 'HT-1', contractName: '合同' }) await flush() h.calls[8].resolve([{ id: 'new-product' }]) h.calls[9].resolve([]) await second assert.equal(s.loadedContractId.value, 'c1') assert.equal(s.loading.value, false) h.stop() }) test('closing dialog invalidates pending searches including their finally state', async () => { const h = scopeDialogHarness(), s = h.state s.visible.value = true const old = s.searchContracts('old') s.visible.value = false s.visible.value = true const fresh = s.searchContracts('new') h.calls[0].resolve([{ id: 'old' }]) await old assert.deepEqual(s.contracts.value, []) assert.equal(s.searching.value, true) h.calls[1].resolve([{ id: 'new' }]) await fresh assert.equal(s.contracts.value[0].id, 'new') assert.equal(s.searching.value, false) h.stop() })