| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- 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()
- })
|