697 lines
23 KiB
TypeScript
697 lines
23 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 viewportWidth = vue.ref(1120)
|
|
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
|
|
},
|
|
useElementSize: () => ({ width: viewportWidth }),
|
|
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, page, total, expansionStyle }`
|
|
)(...Object.values(env))
|
|
)
|
|
return {
|
|
state,
|
|
viewportWidth,
|
|
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(/<el-table :data="usageRows"[\s\S]*?<\/el-table>/)![0]
|
|
const props = [...table.matchAll(/<el-table-column prop="([^"]+)"/g)].map((m) => 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(/<el-tag size="small">\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()
|
|
})
|
|
|
|
// 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()
|
|
})
|
|
|
|
test('refresh and search while permission request is pending do not cancel initialization', async () => {
|
|
const h = harness(),
|
|
s = h.state
|
|
await s.load()
|
|
s.keyword.value = '目标'
|
|
s.setFilterState('contractNo', { text: 'HT' })
|
|
assert.equal(h.calls.length, 0)
|
|
h.permissions[0].resolve({ menuCodes: ['pl:quota-cert:list'] })
|
|
await flush()
|
|
assert.equal(h.calls.length, 1)
|
|
assert.deepEqual(h.calls[0].args[0], { page: 1, pageSize: 20, keyword: '目标', contractNo: 'HT' })
|
|
h.calls[0].resolve({ list: [{ id: 'loaded' }], total: 1 })
|
|
await flush()
|
|
assert.equal(s.rows.value[0].id, 'loaded')
|
|
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 wrapper of [
|
|
'contract-quota/ContractQuotaPanel.vue',
|
|
'certificate-usage/CertificateUsagePanel.vue'
|
|
]) {
|
|
for (const active of [undefined, false])
|
|
test(`compiled ${wrapper} ${active === false ? 'inactive skips requests' : 'defaults to active and loads'}`, async () => {
|
|
const calls: string[] = []
|
|
const dependencies = {
|
|
useUserStore: () => vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } }),
|
|
fetchGetMenuCodes: async () => {
|
|
calls.push('permissions')
|
|
return { all: true }
|
|
},
|
|
quotaStatisticsApi: {
|
|
contracts: async () => {
|
|
calls.push('contracts')
|
|
return { list: [], total: 0 }
|
|
},
|
|
certificates: async () => {
|
|
calls.push('certificates')
|
|
return { list: [], total: 0 }
|
|
}
|
|
},
|
|
usePersistedListColumns,
|
|
useServerColumnFilters,
|
|
createStatisticsLoader,
|
|
useElementSize: () => ({ width: vue.ref(1120) }),
|
|
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
|
statusLabel: statisticsLabels.statusLabel,
|
|
contractTypeLabel: statisticsLabels.contractTypeLabel
|
|
}
|
|
// Compile the real inner setup too; only its visual child tree is omitted here.
|
|
const StatisticsPanel = compiledSfc(
|
|
'../src/views/pl/quota-statistics/StatisticsPanel.vue',
|
|
dependencies,
|
|
false
|
|
)
|
|
StatisticsPanel.render = () => null
|
|
const Wrapper = compiledSfc(`../src/views/pl/${wrapper}`, { StatisticsPanel })
|
|
const renderer = vue.createRenderer<any, any>({
|
|
createElement: () => ({}),
|
|
createText: () => ({}),
|
|
createComment: () => ({}),
|
|
setText: () => {},
|
|
setElementText: () => {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null,
|
|
insert: () => {},
|
|
remove: () => {},
|
|
patchProp: () => {}
|
|
})
|
|
const app = renderer.createApp(Wrapper, active === undefined ? {} : { active })
|
|
app.mount({})
|
|
await flush()
|
|
await vue.nextTick()
|
|
assert.deepEqual(
|
|
calls,
|
|
active === false
|
|
? []
|
|
: ['permissions', wrapper.startsWith('contract') ? 'contracts' : 'certificates']
|
|
)
|
|
app.unmount()
|
|
})
|
|
}
|
|
|
|
test('expanded content follows the table viewport width and keeps alignment local', () => {
|
|
const h = harness()
|
|
assert.equal(h.state.expansionStyle.value.width, '1120px')
|
|
h.viewportWidth.value = 760
|
|
assert.equal(h.state.expansionStyle.value.width, '760px')
|
|
const sfc = parse(
|
|
readFileSync(
|
|
new URL('../src/views/pl/quota-statistics/StatisticsPanel.vue', import.meta.url),
|
|
'utf8'
|
|
)
|
|
).descriptor
|
|
assert.match(
|
|
sfc.template!.content,
|
|
/class="view-section statistics-expansion"\s+:style="expansionStyle"/
|
|
)
|
|
assert.equal(sfc.styles[0].scoped, true)
|
|
assert.match(sfc.styles[0].content, /text-align: left/)
|
|
h.stop()
|
|
})
|