292 lines
9.3 KiB
TypeScript
292 lines
9.3 KiB
TypeScript
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 }`
|
|
)(...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)
|
|
}
|
|
})
|