feat(crm): 新增合同运输计划合法使用证明页签并隔离权限与异步会话。
This commit is contained in:
1 parent
cd33a387a4
commit
144a3cb236
2 files changed
+412
-4
No files matched your search
@@ -0,0 +1,352 @@
|
||||
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()
|
||||
})
|
||||
Reference in new issue
Block a user