Files
shanxia2.0/web-admin/scripts/contract-proof-tab.test.ts
T

353 lines
11 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { parse, compileTemplate } from '@vue/compiler-sfc'
import ts from 'typescript'
import * as vue from 'vue'
const descriptor = parse(
readFileSync(
new URL('../src/views/crm/contract/components/ContractDetailDialog.vue', import.meta.url),
'utf8'
)
).descriptor
const flush = async () => {
for (let i = 0; i < 30; i++) await Promise.resolve()
}
function deferred() {
let resolve!: (value?: any) => void
const promise = new Promise<any>((r) => {
resolve = r
})
return { promise, resolve }
}
function harness() {
const permissions: ReturnType<typeof deferred>[] = [],
contracts: any[] = [],
mounted: any[] = [],
disposed: any[] = []
const user = vue.reactive({
accessToken: 'a',
getUserInfo: { userId: 'u', tenantNo: 't', roles: ['operator'] }
})
const dict = deferred()
const source = ts.createSourceFile(
'detail.ts',
descriptor.scriptSetup!.content,
ts.ScriptTarget.Latest,
true
)
const env: any = Object.fromEntries(
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key))
)
delete env.default
for (const statement of source.statements) {
if (!ts.isImportDeclaration(statement)) continue
const clause = statement.importClause
if (clause?.name) env[clause.name.text] = () => undefined
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
for (const item of clause.namedBindings.elements)
if (!(item.name.text in env)) env[item.name.text] = () => undefined
}
}
Object.assign(env, {
defineProps: () => ({}),
withDefaults: (v: any) => v,
defineEmits: () => () => {},
defineExpose: () => {},
useUserStore: () => user,
useDictStore: () => ({ loadAllDict: () => dict.promise }),
fetchGetMenuCodes: () => {
const p = deferred()
permissions.push(p)
return p.promise
},
getContractDetail: (id: string) => {
const p = deferred()
contracts.push({ id, ...p })
return p.promise
},
listContractApproval: async () => ({ data: [] }),
onMounted: (fn: any) => mounted.push(fn),
onBeforeUnmount: (fn: any) => disposed.push(fn)
})
const code = ts.transpile(
source.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(source))
.join('\n'),
{ target: ts.ScriptTarget.ESNext }
)
const scope = vue.effectScope()
const state = scope.run(() =>
new Function(
...Object.keys(env),
`${code}; return { visible, detailActiveTab, detailContract, currentDetailId, canViewQuotaCert, canViewProof: typeof canViewProof === 'undefined' ? ref(false) : canViewProof, open, loadDetailData, detailLoading, handleDetailCopy, formDialogCopySource }`
)(...Object.values(env))
)
mounted.forEach((fn) => fn())
return {
state,
user,
permissions,
contracts,
dict,
stop() {
disposed.forEach((fn) => fn())
scope.stop()
}
}
}
test('independent permissions initialize without waiting for dictionaries', async () => {
for (const [code, quota, proof] of [
['pl:legal-use-document:list', false, true],
['pl:quota-cert:list', true, false],
['none', false, false]
] as const) {
const h = harness()
h.state.visible.value = true
await flush()
assert.equal(h.permissions.length, 1)
h.permissions[0].resolve({ menuCodes: [code] })
await flush()
assert.equal(h.state.canViewQuotaCert.value, quota)
assert.equal(h.state.canViewProof.value, proof)
h.dict.resolve()
h.stop()
}
})
for (const change of ['tenant', 'token', 'user', 'roles', 'close'])
test(`permissions discard old responses after ${change} and reopen`, async () => {
const h = harness(),
s = h.state
s.visible.value = true
await flush()
assert.equal(h.permissions.length, 1)
if (change === 'tenant') h.user.getUserInfo.tenantNo = 'new'
if (change === 'token') h.user.accessToken = 'new'
if (change === 'roles') h.user.getUserInfo.roles = []
if (change === 'user') h.user.getUserInfo.userId = 'new'
if (change === 'close') {
s.visible.value = false
s.visible.value = true
}
if (change !== 'close') {
assert.equal(s.visible.value, false)
s.visible.value = true
}
await flush()
assert.equal(h.permissions.length >= 2, true)
h.permissions[h.permissions.length - 1].resolve({ menuCodes: ['pl:legal-use-document:list'] })
await flush()
h.permissions[0].resolve({ all: true })
await flush()
assert.equal(s.canViewQuotaCert.value, false)
assert.equal(s.canViewProof.value, true)
s.detailActiveTab.value = 'proof'
s.visible.value = false
assert.equal(s.canViewProof.value, false)
assert.equal(s.detailActiveTab.value, 'contract')
h.stop()
})
test('switching contracts clears old scope immediately and discards reversed detail responses', async () => {
const h = harness(),
s = h.state
const first = s.open('one')
h.contracts[0].resolve({ data: { id: 'one' } })
await first
s.detailActiveTab.value = 'proof'
const second = s.open('two')
assert.equal(s.detailContract.value, undefined)
const third = s.open('three')
h.contracts[2].resolve({ data: { id: 'three' } })
await third
h.contracts[1].resolve({ data: { id: 'two' } })
await second
assert.equal(s.detailContract.value.id, 'three')
h.stop()
})
// Compile the actual three panes to exercise v-if/props/key, without the unrelated 4,000-line dialog.
const template = descriptor.template!.content
const start = template.indexOf('<el-tab-pane label="运输计划"')
const end = template.indexOf('<!-- 回款记录 -->', start)
const compiled = compileTemplate({
source: `<div>${template.slice(start, end)}</div>`,
filename: 'tabs.vue',
id: 'tabs'
})
const renderSource = compiled.code
.replace(
/import \{([^}]+)\} from "vue"/g,
(_, bindings) => `const {${bindings.replace(/ as /g, ':')}} = Vue`
)
.replace('export function render', 'function render')
const render = new Function('Vue', `${renderSource}; return render`)({
...vue,
resolveComponent: (name: string) => ({ name })
})
function panes(ctx: any) {
const tree = render(ctx, [])
return tree.children.filter((v: any) => typeof v.type !== 'symbol')
}
function panel(pane: any) {
return pane.children.default().find((v: any) => typeof v.type !== 'symbol')
}
test('real template keeps plans quota proof order and separate lazy contract scopes', () => {
const ctx = {
visible: true,
currentDetailId: 42,
canViewQuotaCert: true,
canViewProof: true,
detailActiveTab: 'proof',
detailContract: { id: 42 }
}
const rows = panes(ctx)
assert.deepEqual(
rows.map((v: any) => v.props.name),
['plans', 'quota', 'proof']
)
assert.equal(rows[2].props.label, '运输计划合法使用证明')
assert.equal(rows[2].props.lazy, '')
assert.equal(panel(rows[1]), undefined)
const proof = panel(rows[2])
assert.equal(proof.props['contract-id'], '42')
assert.equal(proof.key, 42)
assert.equal(proof.props.active, true)
assert.equal(proof.props.embedded, '')
ctx.detailContract.id = 43
ctx.currentDetailId = 43
assert.equal(panel(panes(ctx)[2]).key, 43)
ctx.canViewQuotaCert = false
assert.deepEqual(
panes(ctx).map((v: any) => v.props.name),
['plans', 'proof']
)
ctx.detailActiveTab = 'quota'
assert.equal(panel(panes(ctx)[1]), undefined)
})
test('closing A and opening B does not accept A detail or invalidate B permissions', async () => {
const h = harness(),
s = h.state
const a = s.open('A')
s.visible.value = false
const b = s.open('B')
h.contracts[1].resolve({ data: { id: 'B' } })
await b
h.permissions[h.permissions.length - 1].resolve({ menuCodes: ['pl:legal-use-document:list'] })
await flush()
h.contracts[0].resolve({ data: { id: 'A' } })
await a
assert.equal(s.detailContract.value.id, 'B')
assert.equal(s.canViewProof.value, true)
h.stop()
})
test('mounted template unmounts old contract and never mounts quota for proof-only users', async () => {
const lifecycle: string[] = []
const Child = (name: string) =>
vue.defineComponent({
props: ['contractId', 'active', 'embedded'],
setup(props) {
lifecycle.push(`mount:${name}:${props.contractId}`)
vue.onBeforeUnmount(() => lifecycle.push(`unmount:${name}:${props.contractId}`))
return () => vue.h('span')
}
})
const components: Record<string, any> = {
'el-tab-pane': vue.defineComponent({
setup(_, { slots }) {
return () => vue.h('section', slots.default?.())
}
}),
StopDocuments: Child('proof'),
ContractQuotaPanel: Child('quota'),
PlContractLinkedPlansPanel: vue.defineComponent({
setup() {
return () => vue.h('span')
}
})
}
const mountedRender = new Function('Vue', `${renderSource}; return render`)({
...vue,
resolveComponent: (name: string) => components[name]
})
const ctx = vue.reactive<any>({
visible: true,
currentDetailId: 'A',
canViewQuotaCert: false,
canViewProof: true,
detailActiveTab: 'proof',
detailContract: { id: 'A' }
})
const renderer = vue.createRenderer<any, any>({
createElement: () => ({ children: [] }),
createText: () => ({}),
createComment: () => ({}),
insert(node, parent, anchor) {
node.parent = parent
const old = parent.children.indexOf(node)
if (old >= 0) parent.children.splice(old, 1)
const i = parent.children.indexOf(anchor)
parent.children.splice(i < 0 ? parent.children.length : i, 0, node)
},
remove(node) {
const list = node.parent?.children
if (list) list.splice(list.indexOf(node), 1)
},
setText() {},
setElementText() {},
patchProp() {},
parentNode: (node) => node.parent,
nextSibling(node) {
return node.parent?.children[node.parent.children.indexOf(node) + 1]
}
})
const app = renderer.createApp({ setup: () => () => mountedRender(ctx, []) })
app.mount({ children: [] })
assert.deepEqual(lifecycle, ['mount:proof:A'])
ctx.currentDetailId = 'B'
await vue.nextTick()
assert.deepEqual(lifecycle, ['mount:proof:A', 'unmount:proof:A'])
ctx.detailContract = { id: 'B' }
await vue.nextTick()
assert.equal(lifecycle.at(-1), 'mount:proof:B')
ctx.detailActiveTab = 'contract'
await vue.nextTick()
assert.equal(lifecycle.at(-1), 'unmount:proof:B')
assert.equal(
lifecycle.some((entry) => entry.includes('quota')),
false
)
app.unmount()
})
test('copy action retains the loaded contract after hiding the detail dialog', async () => {
const h = harness(),
s = h.state
const opened = s.open('copy')
h.contracts[0].resolve({ data: { id: 'copy' } })
await opened
s.handleDetailCopy()
assert.equal(s.formDialogCopySource.value?.id, 'copy')
h.stop()
})
test('session change closes loading detail and old finally cannot revive it', async () => {
const h = harness(),
s = h.state
const pending = s.open('old')
assert.equal(s.detailLoading.value, true)
h.user.getUserInfo.tenantNo = 'another'
assert.equal(s.visible.value, false)
assert.equal(s.detailLoading.value, false)
h.contracts[0].resolve({ data: { id: 'old' } })
await pending
assert.equal(s.detailContract.value, undefined)
h.stop()
})