Files
shanxia2.0/web-admin/scripts/mobile-plan-contract-embedded.test.ts
T
2026-10-23 23:07:00 +08:00

146 lines
5.3 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
import { compile, createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import ts from 'typescript'
const path = new URL('../src/views/m/crm/contract/detail.vue', import.meta.url)
const d = parse(readFileSync(path, 'utf8')).descriptor
const ast = ts.createSourceFile('detail.ts', d.scriptSetup!.content, ts.ScriptTarget.Latest, true)
function fn(name: string, bindings: any) {
const node = ast.statements.find((n) => ts.isFunctionDeclaration(n) && n.name?.text === name)!
return new Function(
...Object.keys(bindings),
ts.transpile(node.getText(ast)) + `;return ${name}`
)(...Object.values(bindings))
}
test('embedded identity comes only from recordId and normal identity stays route based', () => {
const props = { embedded: true, recordId: 'contract' }
const route = { params: { id: 'plan' } }
const current = fn('currentRecordId', { props, route })
assert.equal(current(), 'contract')
props.embedded = false
assert.equal(current(), 'plan')
})
test('embedded route actions never navigate or write', async () => {
let calls = 0
await fn('runRouteAction', {
props: { embedded: true },
route: { query: { action: 'delete' } },
captureOperation: () => {
calls++
return {}
},
router: {
replace: async () => {
calls++
}
},
isCurrentOperation: () => true,
runAction: () => calls++
})()
assert.equal(calls, 0)
})
test('embedded template reuses complete detail, readonly attachments and masks all numeric pricing regions', async () => {
compileScript(d, { id: 'embedded' })
assert.deepEqual(
compileTemplate({ source: d.template!.content, filename: path.pathname, id: 'embedded' })
.errors,
[]
)
const template = d.template!.content
assert.match(template, /:editable="!props.embedded && !isApproveEntry"/)
assert.match(template, /priceTiers/)
for (const id of ['amount', 'products']) {
const fragment = template.slice(
template.indexOf(`<section id="contract-sec-${id}"`),
template.indexOf('</section>', template.indexOf(`<section id="contract-sec-${id}"`)) + 10
)
const bindings = {
props: { embedded: true, hideProductPrice: true },
contract: { amount: 9876, freight: 9876, barrelDeposit: 9876 },
products: [{ productName: '产品', price: 9876, priceTiers: [{ price: 9876 }] }],
formatContractMoney: () => '9876',
productMoney: () => '9876',
displayCategory: () => '',
unitLabel: () => '',
formatBrandConstraintLabel: () => '',
isFrameworkContractType: () => true
}
const render = compile(fragment)
const html = await renderToString(
createSSRApp({ render: () => render.call(bindings, bindings, []) })
)
assert.doesNotMatch(html, /9876/)
}
})
test('embedded real Vue identity watcher ignores plan route changes and rejects old request after switch or unmount', async () => {
const { reactive, ref, watch, nextTick, effectScope } = await import('vue')
const props = reactive({ embedded: true, recordId: 'A' })
const route = reactive({ params: { id: 'plan' }, query: {} })
let resolveOld: (v: any) => void = () => {}
const old = new Promise((r) => {
resolveOld = r
})
const hooks: Array<() => void> = []
const names = ['currentRecordId', 'isCurrentLoad', 'loadDetail', 'resetRecord', 'runRouteAction']
const declarations = ast.statements
.filter((n) => ts.isFunctionDeclaration(n) && names.includes(n.name?.text || ''))
.map((n) => n.getText(ast))
const lifecycle = ast.statements
.filter(
(n) =>
ts.isExpressionStatement(n) &&
((n.getText(ast).startsWith('watch(') &&
n.getText(ast).includes('() => currentRecordId()')) ||
n.getText(ast).startsWith('onBeforeUnmount('))
)
.map((n) => n.getText(ast))
const calls: string[] = []
const bindings = {
props,
route,
watch,
onBeforeUnmount: (cb: () => void) => hooks.push(cb),
contract: ref<any>(null),
products: ref<any[]>([]),
approvalList: ref<any[]>([]),
categoryPaths: ref(new Map()),
errorText: ref(''),
loading: ref(false),
closeRecordActions: () => {},
loadCategoryPaths: async () => {},
getContract: async (id: string) => {
calls.push(id)
return id === 'A' ? old : { id, products: [] }
},
listContractApproval: async () => ({ data: [] }),
getQuote: async () => ({})
}
const scope = effectScope()
const run = scope.run(() =>
new Function(
...Object.keys(bindings),
'let detailRequestSeq=0;let shellReady=true;let disposed=false;' +
ts.transpile([...declarations, ...lifecycle].join('\n')) +
';return {loadDetail}'
)(...Object.values(bindings))
)
const pending = run.loadDetail()
route.params.id = 'other-plan'
await nextTick()
assert.deepEqual(calls, ['A'])
props.recordId = 'B'
await nextTick()
await new Promise((r) => setTimeout(r, 0))
assert.equal(bindings.contract.value?.id, 'B')
resolveOld({ id: 'A', products: [{ id: 'stale' }] })
await pending
assert.equal(bindings.contract.value?.id, 'B')
hooks.forEach((h) => h())
assert.equal(bindings.contract.value, null)
scope.stop()
})