Files
shanxia2.0/web-admin/scripts/mobile-quote-approval-guards.test.ts
T

175 lines
5.8 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { parse } from '@vue/compiler-sfc'
import * as Vue from 'vue'
import ts from 'typescript'
import { findPendingQuoteApproval } from '../src/utils/mp-crm-quote-card'
function deferred() {
let resolve!: (value?: any) => void
const promise = new Promise<any>((r) => {
resolve = r
})
return { promise, resolve }
}
async function flush() {
for (let i = 0; i < 30; i++) await Promise.resolve()
}
function harness(overrides: Record<string, any> = {}) {
const source = readFileSync(
new URL('../src/views/m/crm/quote/detail.vue', import.meta.url),
'utf8'
)
const ast = ts.createSourceFile(
'detail.ts',
parse(source).descriptor.scriptSetup!.content,
ts.ScriptTarget.Latest,
true
)
const statements = ast.statements.filter((s) => !ts.isImportDeclaration(s))
const names = statements.flatMap((s) =>
ts.isFunctionDeclaration(s) && s.name
? [s.name.text]
: ts.isVariableStatement(s)
? s.declarationList.declarations.map((d) => d.name.getText(ast))
: []
)
const route = Vue.reactive({
name: 'MpCrmQuoteDetail',
path: '/m/crm/quote/Q1',
params: { id: 'Q1' },
query: { from: 'approve' } as Record<string, string>
})
const user = Vue.reactive({ accessToken: 'token', info: { id: 'user', tenantNo: 'T1' } })
const writes: any[] = []
const unmounts: (() => void)[] = []
const bindings: Record<string, any> = { ...Vue }
for (const s of ast.statements)
if (ts.isImportDeclaration(s)) {
if (s.importClause?.name) bindings[s.importClause.name.text] = () => {}
const n = s.importClause?.namedBindings
if (n && ts.isNamedImports(n)) for (const e of n.elements) bindings[e.name.text] = () => {}
}
Object.assign(
bindings,
{
useRoute: () => route,
useRouter: () => ({
push: () => {},
replace: async ({ query }: any) => {
route.query = query
}
}),
useUserStore: () => user,
useDictStore: () => ({ getLabel: (_: any, value: any) => value, getTagType: () => 'info' }),
getQuote: async (id: string) => ({ data: { id, status: 'submitted', productList: [] } }),
listApproval: async () => ({
data: [{ id: 'approval-1', step: 1, approverId: 'user', approvalResult: 'pending' }]
}),
fetchCategoryTree: async () => [],
buildPathLabelMapFromTreeRoots: () => new Map(),
listQuoteProduct: async () => ({ rows: [] }),
showCrmConfirmDialog: async () => {},
approveQuote: async (q: any) => writes.push(q),
findPendingQuoteApproval,
onUnmounted: () => {},
onMounted: () => {},
onBeforeUnmount: (fn: () => void) => unmounts.push(fn),
document: { querySelector: () => null },
window: { removeEventListener: () => {} }
},
overrides
)
for (const key of Object.keys(bindings))
if (!/^[a-zA-Z_$][\w$]*$/.test(key) || key === 'default') delete bindings[key]
const scope = Vue.effectScope()
const code = ts.transpile(statements.map((s) => s.getText(ast)).join('\n'), {
target: ts.ScriptTarget.ESNext
})
const api = scope.run(() =>
new Function(...Object.keys(bindings), `${code};return {${names.join(',')}}`)(
...Object.values(bindings)
)
)
return {
api,
route,
user,
writes,
source,
stop: () => {
unmounts.forEach((fn) => fn())
scope.stop()
}
}
}
test('detail denies explicit false permission but supports omitted detail flags with current-step approval', async () => {
for (const flag of [false, undefined, true]) {
const h = harness({
getQuote: async (id: string) => ({ data: { id, status: 'submitted', canApprove: flag } })
})
try {
h.api.onShellReady()
await flush()
assert.equal(
h.api.fabActions.value.some((a: any) => a.key === 'approve'),
flag !== false
)
await h.api.runAction('approve')
await h.api.confirmApprove()
assert.equal(h.writes.length, flag === false ? 0 : 1)
} finally {
h.stop()
}
}
})
test('identity change clears detail immediately and cancels an open approval confirmation', async () => {
const confirmation = deferred()
const h = harness({ showCrmConfirmDialog: () => confirmation.promise })
try {
h.api.onShellReady()
await flush()
await h.api.runAction('approve')
const pending = h.api.confirmApprove()
h.user.info.tenantNo = 'T2'
assert.equal(h.api.quote.value, null)
assert.equal(h.api.approveOpen.value, false)
confirmation.resolve()
await pending
assert.deepEqual(h.writes, [])
} finally {
h.stop()
}
})
test('approval history error is retryable and a late history response cannot cross identity changes', async () => {
let failing = true
const history = deferred()
let pendingRead = false
const h = harness({
listApproval: async () => {
if (failing) throw new Error('审批历史离线')
if (pendingRead) return history.promise
return { data: [{ id: 'node', step: 1, approverId: 'user', approvalResult: 'pending' }] }
}
})
try {
h.api.onShellReady()
await flush()
assert.ok(h.api.approvalError, 'history must have a distinct error state')
assert.equal(h.api.approvalError.value, '审批历史离线')
assert.equal(h.api.pendingApproval.value, null)
failing = false
await h.api.retryApprovalHistory()
assert.equal(h.api.approvalError.value, '')
assert.equal(h.api.pendingApproval.value.id, 'node')
pendingRead = true
const retry = h.api.retryApprovalHistory()
h.user.accessToken = ''
history.resolve({ data: [{ id: 'stale', approverId: 'user', approvalResult: 'pending' }] })
await retry
assert.deepEqual(h.api.approvalList.value, [])
} finally {
h.stop()
}
})