import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' import { parse } from '@vue/compiler-sfc' import ts from 'typescript' import * as vue from 'vue' function deferred() { let resolve!: (value?: any) => void let reject!: (error: any) => void const promise = new Promise((a, b) => { resolve = a reject = b }) return { promise, resolve, reject } } const flush = async () => { for (let i = 0; i < 20; i++) await Promise.resolve() } function harness(page: string, overrides: Record = {}, initialProps: any = {}) { const content = parse( readFileSync(new URL(`../src/views/m/pl/${page}.vue`, import.meta.url), 'utf8') ).descriptor.scriptSetup!.content const ast = ts.createSourceFile('page.ts', content, ts.ScriptTarget.Latest, true) const route = vue.reactive({ params: { id: 'A' }, query: {} as Record, fullPath: '/A', path: '/A' }) const props = vue.reactive({ modelValue: true, planId: 'A', permitted: true, ...initialProps }) const messages: any[] = [] const unmounts: Array<() => void> = [] const writes: any[] = [], pushes: any[] = [], calls: string[] = [] const bindings: Record = Object.fromEntries( Object.entries(vue).filter( ([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default' && key !== 'version' ) ) for (const s of ast.statements) { if (!ts.isImportDeclaration(s)) continue const clause = s.importClause if (clause?.name) bindings[clause.name.text] = () => undefined if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const item of clause.namedBindings.elements) bindings[item.name.text] = () => undefined } const read = async (arg: any) => { const id = typeof arg === 'string' ? arg : arg.id calls.push(id) return { data: { id, status: 'draft', products: [{ id: 'p', productId: 'p', isToxic: 1 }], productList: [] } } } const planApi = { get: read, getArrangeInfo: async () => ({ data: {} }), waitNotice: async (id: string) => { writes.push(id) return { data: { id, products: [] } } }, cancelWaitNotice: async (id: string) => { writes.push(id) return { data: { id, products: [] } } }, cancel: async (...args: any[]) => { writes.push(args) return { data: { id: args[0], products: [] } } }, setHazardCertReady: async (...args: any[]) => writes.push(args), updateProductCerts: async (...args: any[]) => writes.push(args), ...overrides.planApi } Object.assign(bindings, { useRoute: () => route, useRouter: () => ({ push: (v: any) => pushes.push(v), replace: async (v: any) => { if (typeof v === 'object') route.query = v.query else pushes.push(v) } }), useUserStore: () => ({ accessToken: 'session', info: { id: 'user' } }), useDictStore: () => ({ getLabel: () => '', getTagType: () => '' }), onMounted() {}, onUnmounted: (fn: any) => unmounts.push(fn), onBeforeUnmount: (fn: any) => unmounts.push(fn), loadMobileDetailReads: async () => ({ errors: [] }), resolvePlanStopsForList: (plan: any) => ({ load: plan.load || {}, deliveries: plan.deliveries || [] }), mapPlanProductToFormRow: (product: any) => product, mapLoadFromApi: (load: any) => load, mapDeliveriesFromApi: (deliveries: any[]) => deliveries, resolveStopProductsForView: (products: any[]) => products || [], resolveLoadViewAddress: (_plan: any, load: any) => load.address || '', resolveDeliveryViewAddress: (_plan: any, stop: any) => stop.address || '', getContract: read, getQuote: read, fetchProductDetail: read, listContractApproval: async () => ({ data: [] }), ElMessage: Object.fromEntries( ['warning', 'error', 'success', 'info'].map((k) => [ k, (...args: any[]) => messages.push([k, ...args]) ]) ), fetchGetMenuCodes: async () => ({ all: true }), canEditPlan: () => true, canArrangeTransport: () => true, canWaitNotice: () => true, canCancelWaitNotice: () => true, canVoidPlan: () => true, canEdit: (p: any) => !!p, canSave: (a: boolean, u: boolean, s: boolean, n: number) => a && !u && !s && n > 0, certPayload: (p: any) => JSON.parse(JSON.stringify(p)), isHazardProduct: (p: any) => !!p.isToxic, enrichPlanProductsHazardFromCrm: async (p: any) => p, isHazardCertReady: () => false, planHasHazardProducts: (p: any) => !!p?.length, buildPlanCard: () => ({}), splitMpPlanDetailActions: () => ({ primary: [], more: [] }), usePlPlanArrangeActions: (options: any) => arrangeHarness(planApi, options), showCrmConfirmDialog: async () => {}, defineExpose() {}, defineProps: () => props, defineEmits: () => (...v: any[]) => pushes.push(v), document: { querySelector: () => null }, window: { removeEventListener() {}, scrollTo() {} }, ...overrides, planApi }) const statements = ast.statements.filter((s) => !ts.isImportDeclaration(s)) const names = statements.flatMap((s) => ts.isFunctionDeclaration(s) ? [s.name!.text] : ts.isVariableStatement(s) ? s.declarationList.declarations.flatMap((d) => ts.isObjectBindingPattern(d.name) ? d.name.elements.map((e) => e.name.getText(ast)) : [d.name.getText(ast)] ) : [] ) const code = ts.transpile(statements.map((s) => s.getText(ast)).join('\n'), { target: ts.ScriptTarget.ESNext }) const scope = vue.effectScope() const api: any = scope.run(() => new Function(...Object.keys(bindings), `${code};return {${names.join(',')}}`)( ...Object.values(bindings) ) ) return { ...api, props: bindings.defineProps(), messages, planApi, route, calls, writes, pushes, ready: api.onShellReady || api.onReady, unmount: () => { unmounts.forEach((fn) => fn()) scope.stop() } } } function arrangeHarness(planApi: any, options: any = undefined, overrides: any = {}) { const text = readFileSync( new URL('../src/composables/usePlPlanArrangeActions.ts', import.meta.url), 'utf8' ) const ast = ts.createSourceFile('arrange.ts', text, ts.ScriptTarget.Latest, true) const code = ts.transpile( ast.statements .filter((s) => !ts.isImportDeclaration(s)) .map((s) => s.getText(ast)) .join('\n') .replace(/export /g, ''), { target: ts.ScriptTarget.ESNext } ) const env = { ...Object.fromEntries( Object.entries(vue).filter(([k]) => /^[a-zA-Z_$][\w$]*$/.test(k) && k !== 'version') ), planApi, pickBxData: (r: any) => r.data ?? r, useUserStore: () => ({ accessToken: 'session', info: { id: 'user' } }), useDictStore: () => ({ getLabel: () => '' }), ElMessage: { success() {}, warning() {}, error() {} }, ...overrides } delete (env as any).default return new Function(...Object.keys(env), `${code}; return usePlPlanArrangeActions`)( ...Object.values(env) )(options) } test('transport: ready gate, idempotence and sync reset', async () => { const h = harness('plan/detail') h.route.params.id = 'B' await flush() assert.deepEqual(h.calls, []) h.onShellReady() await flush() h.onShellReady() await flush() assert.deepEqual(h.calls, ['B']) h.moreOpen.value = h.voidOpen.value = h.vehicleOpen.value = true h.route.params.id = 'C' assert.equal(h.plan.value, null) assert.equal(h.moreOpen.value, false) assert.equal(h.voidOpen.value, false) assert.equal(h.vehicleOpen.value, false) await flush() assert.deepEqual(h.calls, ['B', 'C']) h.route.query.tab = 'info' await flush() assert.deepEqual(h.calls, ['B', 'C']) h.unmount() }) for (const result of [null, {}, { id: 'WRONG' }]) test(`transport invalid success ${JSON.stringify(result)}`, async () => { const h = harness('plan/detail', { planApi: { get: async () => ({ data: result }) } }) h.onShellReady() await flush() assert.equal(h.plan.value, null) assert.ok(h.errorText.value) h.unmount() }) for (const kind of ['wait', 'cancel-wait', 'void']) test(`transport ${kind} delayed confirmation`, async () => { const confirm = deferred() const h = harness('plan/detail', { showCrmConfirmDialog: () => confirm.promise }) h.onShellReady() await flush() const pending = kind === 'void' ? h.confirmVoidPlan() : h.runAction(kind) confirm.resolve() h.route.params.id = 'B' await pending await flush() assert.deepEqual(h.writes, []) h.unmount() }) test('arrange opt-in old internal info cannot publish after reset', async () => { const old = deferred() const a = arrangeHarness({ getArrangeInfo: () => old.promise }, { isolateRecord: true }) const pending = a.loadArrangeActions('A', { id: 'A', transportStatus: 'arranged' }) a.resetArrangeActions() old.resolve({ data: { driverUrl: 'OLD', siteRecords: [{ siteUrl: 'OLD' }] } }) await pending assert.equal(a.driverUrl.value, '') assert.deepEqual(a.siteRecords.value, []) }) test('cert parent delayed confirmation invalidated synchronously', async () => { const confirm = deferred() const h = harness('plan-cert/detail', { showCrmConfirmDialog: () => confirm.promise }) await h.init() const pending = h.setReady({ id: 'A' }, true) await flush() confirm.resolve() h.route.params.id = 'B' await pending assert.deepEqual(h.writes, []) h.unmount() }) test('cert upload old successful save cannot close or notify new record', async () => { const saved = deferred() const h = harness('plan-cert/CertUpload', { planApi: { updateProductCerts: () => saved.promise } }) await flush() const pending = h.save() h.props.planId = 'B' await flush() saved.resolve() await pending assert.deepEqual(h.pushes, []) assert.deepEqual(h.messages, []) h.unmount() }) function detailLoader(read: (id: string) => Promise, useInitialPlan = false) { return () => { const state: any = Object.fromEntries( [ 'plan', 'products', 'transport', 'freightSettlement', 'vehicleBundle', 'dibangRows', 'ioOrder' ].map((k) => [k, vue.ref(k === 'products' || k === 'dibangRows' ? [] : null)]) ) state.loading = vue.ref(false) state.resetDetail = () => { state.plan.value = null } state.loadDetail = async (id: string, options?: any) => { state.loading.value = true try { state.plan.value = useInitialPlan && options?.initialPlan ? options.initialPlan : await read(id) } finally { state.loading.value = false } } return state } } for (const reject of [false, true]) test(`cert detail unmount guards old ${reject ? 'error' : 'success'} and contract`, async () => { const slow = deferred(), contract = deferred() const h = harness('plan-cert/CertDetail', { usePlPlanViewDetail: detailLoader(() => slow.promise), getContract: () => contract.promise }) await flush() const pending = h.openContract('C') h.unmount() if (reject) slow.reject(new Error('old')) else slow.resolve({ id: 'A' }) contract.resolve({ data: { id: 'C' } }) await pending await flush() assert.equal(h.contractOpen.value, false) assert.equal(h.error.value, '') assert.equal(h.plan.value, null) }) for (const reject of [false, true]) for (const transition of ['B', 'ABA', 'retry', 'unmount']) test(`transport ${transition}: old ${reject ? 'error/finally' : 'success'} cannot publish`, async () => { const old = deferred(), fresh = deferred() let n = 0 const h = harness('plan/detail', { planApi: { get: () => (++n === 1 ? old.promise : fresh.promise) } }) h.onShellReady() await flush() if (transition === 'unmount') h.unmount() else if (transition === 'retry') void h.loadDetail() else { h.route.params.id = 'B' if (transition === 'ABA') h.route.params.id = 'A' } if (reject) old.reject(new Error('old')) else old.resolve({ data: { id: 'A' } }) await flush() assert.equal(h.plan.value, null) assert.equal(h.errorText.value, '') if (transition !== 'unmount') { assert.equal(h.loading.value, true) fresh.resolve({ data: { id: h.route.params.id } }) await flush() assert.equal(h.plan.value.id, h.route.params.id) assert.equal(h.loading.value, false) h.unmount() } }) for (const kind of ['wait', 'cancel-wait', 'void']) for (const reject of [false, true]) test(`transport ${kind}: old issued write ${reject ? 'rejects' : 'resolves'} after switching`, async () => { const old = deferred() const apiName = kind === 'wait' ? 'waitNotice' : kind === 'void' ? 'cancel' : 'cancelWaitNotice' const h = harness('plan/detail', { planApi: { [apiName]: () => old.promise } }) h.onShellReady() await flush() const pending = kind === 'void' ? h.confirmVoidPlan() : h.runAction(kind) await flush() h.route.params.id = 'B' await flush() h.acting.value = true h.voidOpen.value = true if (reject) old.reject(new Error('old')) else old.resolve({ data: { id: 'A' } }) await pending assert.equal(h.plan.value.id, 'B') assert.equal(h.acting.value, true) assert.equal(h.voidOpen.value, true) assert.deepEqual(h.messages, []) assert.deepEqual(h.calls, ['A', 'B']) h.unmount() }) for (const kind of ['wait', 'cancel-wait', 'void']) test(`transport ${kind}: cancelled confirmation is handled`, async () => { const h = harness('plan/detail', { showCrmConfirmDialog: async () => { throw new Error('cancel') } }) h.onShellReady() await flush() await (kind === 'void' ? h.confirmVoidPlan() : h.runAction(kind)) assert.deepEqual(h.writes, []) assert.equal(h.acting.value, false) assert.deepEqual(h.messages, []) h.unmount() }) test('transport reason is captured before confirmation; unrelated query does not cancel', async () => { const confirm = deferred(), h = harness('plan/detail', { showCrmConfirmDialog: () => confirm.promise }) h.onShellReady() await flush() h.voidReason.value = 'original' const pending = h.confirmVoidPlan() h.voidReason.value = 'changed' h.route.query.tab = 'x' confirm.resolve() await pending assert.deepEqual(h.writes, [['A', { cancelReason: 'original' }]]) h.unmount() }) test('transport entry rejects missing loading busy and denied actions', async () => { const h = harness('plan/detail', { canWaitNotice: () => false, canCancelWaitNotice: () => false, canVoidPlan: () => false }) await h.runAction('wait') h.onShellReady() await flush() for (const kind of ['wait', 'cancel-wait', 'void']) await h.runAction(kind) await h.confirmVoidPlan() assert.deepEqual(h.writes, []) h.unmount() }) test('arrange opt-in ABA cancels old get and reuses existing plan', async () => { const old = deferred() let reads = 0 const a = arrangeHarness( { get: () => { reads++ return old.promise }, getArrangeInfo: async (id: string) => ({ data: { driverUrl: id } }) }, { isolateRecord: true } ) const pending = a.loadArrangeActions('A') await a.loadArrangeActions('B', { id: 'B', transportStatus: 'arranged' }) await a.loadArrangeActions('A', { id: 'A', transportStatus: 'arranged' }) old.resolve({ data: { id: 'A', transportStatus: 'pending' } }) await pending assert.equal(a.driverUrl.value, 'A') assert.equal(a.plan.value.transportStatus, 'arranged') assert.equal(reads, 1) }) test('arrange default PC retains legacy publication and lookup behavior', async () => { const old = deferred() let reads = 0 const a = arrangeHarness({ get: async () => { reads++ return { data: { id: 'A', transportStatus: 'arranged' } } }, getArrangeInfo: () => old.promise }) const pending = a.loadArrangeActions('A') await flush() a.resetArrangeActions() old.resolve({ data: { driverUrl: 'legacy' } }) await pending assert.equal(a.driverUrl.value, 'legacy') assert.equal(reads, 1) }) test('arrange QR nextTick cannot copy newly bound dialog after reset', async () => { let copies = 0 const a = arrangeHarness({}, { isolateRecord: true }) const pending = a.copyPoundSlipQrcodeForRecord({ dbInventoryId: 'A' }) a.resetArrangeActions() a.qrcodeDialogRef.value = { copyQrcodeImage: () => { copies++ } } await pending assert.equal(copies, 0) assert.equal(a.qrcodeVisible.value, false) }) for (const stage of ['get', 'confirm', 'write']) for (const reject of [false, true]) test(`cert ready ${stage}: switched/ABA late ${reject ? 'error' : 'success'} ignored`, async () => { const old = deferred(), writes: any[] = [] const h = harness('plan-cert/detail', { showCrmConfirmDialog: stage === 'confirm' ? () => old.promise : async () => {}, planApi: { ...(stage === 'get' ? { get: () => old.promise } : {}), setHazardCertReady: (...args: any[]) => { writes.push(args) return stage === 'write' ? old.promise : Promise.resolve() } } }) await h.init() const pending = h.setReady({ id: 'A' }, true) await flush() h.route.params.id = 'B' h.route.params.id = 'A' h.acting.value = true if (reject) old.reject(new Error('old')) else old.resolve({ data: { id: 'A', products: [{ isToxic: 1 }] } }) await pending assert.equal(writes.length, stage === 'write' ? 1 : 0) assert.deepEqual(h.messages, []) assert.equal(h.acting.value, true) h.unmount() }) for (const bad of [null, {}, { id: 'B', products: [{}] }]) test(`cert ready invalid read ${JSON.stringify(bad)}`, async () => { const h = harness('plan-cert/detail', { planApi: { get: async () => ({ data: bad }) } }) await h.init() await h.setReady({ id: 'A' }, true) assert.deepEqual(h.writes, []) assert.equal(h.acting.value, false) h.unmount() }) test('cert parent ready idempotence, mismatched event, saved identity and query gate', async () => { let permissionReads = 0, reloads = 0, opens = 0 const h = harness('plan-cert/detail', { fetchGetMenuCodes: async () => { permissionReads++ return { all: true } } }) h.detailRef.value = { reload: () => { reloads++ }, openContract: () => { opens++ }, invalidateRecord() {} } h.route.query.contractId = 'contract' await flush() assert.equal(opens, 0) await h.init() await h.init() assert.equal(permissionReads, 1) await h.setReady({ id: 'OTHER' }, true) assert.deepEqual(h.calls, []) await h.saved('OTHER') assert.equal(reloads, 0) await h.saved('A') assert.equal(reloads, 1) h.unmount() }) for (const reject of [false, true]) test(`cert permissions retry supersedes old ${reject ? 'failure' : 'success'}`, async () => { const old = deferred() let n = 0 const h = harness('plan-cert/detail', { fetchGetMenuCodes: () => (++n === 1 ? old.promise : Promise.resolve({ all: false })) }) const pending = h.init() await h.loadPermissions() if (reject) old.reject(new Error('old')) else old.resolve({ all: true }) await pending assert.equal(h.canReady.value, false) assert.equal(h.permissionError.value, false) assert.equal(h.permissionsLoaded.value, true) h.unmount() }) for (const transition of ['B', 'ABA', 'close-reopen', 'unmount', 'invalidate', 'permission']) for (const reject of [false, true]) test(`upload ${transition}: stale save ${reject ? 'error' : 'success'}`, async () => { const old = deferred() const h = harness('plan-cert/CertUpload', { planApi: { updateProductCerts: () => old.promise } }) await flush() const pending = h.save() if (transition === 'B' || transition === 'ABA') { h.props.planId = 'B' if (transition === 'ABA') h.props.planId = 'A' } if (transition === 'close-reopen') { h.props.modelValue = false h.props.modelValue = true } if (transition === 'unmount') h.unmount() if (transition === 'invalidate') h.invalidateRecord() if (transition === 'permission') h.props.permitted = false await flush() h.saving.value = true if (reject) old.reject(new Error('old')) else old.resolve() await pending assert.deepEqual(h.pushes, []) assert.deepEqual(h.messages, []) assert.equal(h.error.value, '') assert.equal(h.saving.value, true) if (transition !== 'unmount') h.unmount() }) for (const invalid of [{ id: 'B', products: [] }, { id: 'A' }, null]) test(`upload invalid metadata response ${JSON.stringify(invalid)} blocks saving`, async () => { const h = harness('plan-cert/CertUpload', { planApi: { get: async () => ({ data: invalid }) } }) await flush() await h.save() assert.ok(h.error.value) assert.equal(h.allowed.value, false) assert.deepEqual(h.writes, []) h.unmount() }) test('upload strict enrichment failure is explicit, retriable and blocks write', async () => { let strictArg: any const h = harness('plan-cert/CertUpload', { enrichPlanProductsHazardFromCrm: async (_: any, strict: any) => { strictArg = strict throw new Error('metadata') } }) await flush() await h.save() assert.equal(strictArg, true) assert.ok(h.error.value) assert.deepEqual(h.writes, []) h.unmount() }) test('upload keeps edits when same ID/visibility and unrelated query change; payload is captured', async () => { const old = deferred(), writes: any[] = [] const h = harness('plan-cert/CertUpload', { planApi: { updateProductCerts: (...args: any[]) => { writes.push(args) return old.promise } } }) await flush() h.products.value[0].purchaseCertUrl = 'original' h.props.planId = 'A' h.props.modelValue = true h.route.query.tab = 'info' await flush() assert.equal(h.products.value[0].purchaseCertUrl, 'original') assert.equal(h.calls.length, 1) const pending = h.save() h.products.value[0].purchaseCertUrl = 'changed' old.resolve() await pending assert.equal(writes[0][0], 'A') assert.equal(writes[0][1][0].purchaseCertUrl, 'original') assert.deepEqual(h.pushes, [ ['update:modelValue', false], ['success', 'A'] ]) h.unmount() }) for (const reject of [false, true]) test(`upload stale metadata ${reject ? 'error' : 'success'} cannot clear new loading`, async () => { const old = deferred(), fresh = deferred() let n = 0 const h = harness('plan-cert/CertUpload', { planApi: { get: () => (++n === 1 ? old.promise : fresh.promise) } }) h.props.planId = 'B' if (reject) old.reject(new Error('old')) else old.resolve({ data: { id: 'A', products: [] } }) await flush() assert.equal(h.loading.value, true) assert.equal(h.plan.value, null) assert.equal(h.error.value, '') await h.save() assert.deepEqual(h.writes, []) fresh.resolve({ data: { id: 'B', products: [] } }) await flush() assert.equal(h.plan.value.id, 'B') h.unmount() }) function recordChildKey(parent: any, component: string) { const template = parse( readFileSync(new URL('../src/views/m/pl/plan-cert/detail.vue', import.meta.url), 'utf8') ).descriptor.template!.content const key = template.match(new RegExp(`<${component}\\b[^>]*:key="([^"]+)"`))?.[1] assert.ok(key, `${component} must have a record-instance key`) return new Function('state', `with (state) { return (${key}) }`)(vue.proxyRefs(parent)) } for (const change of ['record', 'ABA', 'query']) test(`cert upload real parent renderer ${change} before child props flush`, async () => { const saved = deferred() let child: any const Child = vue.defineComponent({ props: ['modelValue', 'planId', 'permitted'], emits: ['update:modelValue', 'success'], setup(props, context) { child = harness('plan-cert/CertUpload', { defineProps: () => props, defineEmits: () => context.emit, defineExpose: context.expose, onBeforeUnmount: vue.onBeforeUnmount, planApi: { updateProductCerts: () => saved.promise } }) return () => vue.h('upload') } }) const renderer = vue.createRenderer({ insert(el: any, parent: any) { el.parent = parent }, remove() {}, createElement: () => ({}), createText: () => ({}), createComment: () => ({}), setText() {}, setElementText() {}, parentNode: (n: any) => n.parent, nextSibling: () => null, patchProp() {} }) const template = parse( readFileSync(new URL('../src/views/m/pl/plan-cert/detail.vue', import.meta.url), 'utf8') ).descriptor.template!.content assert.match(template, /]*ref="uploadRef"/) const parent = harness('plan-cert/detail') await parent.init() parent.uploadOpen.value = true let successes = 0 const App = vue.defineComponent({ setup: () => () => vue.h(Child, { key: recordChildKey(parent, 'CertUpload'), ref: parent.uploadRef, planId: parent.id.value, modelValue: parent.uploadOpen.value, permitted: parent.canUpload.value, 'data-tab': parent.route.query.tab, 'onUpdate:modelValue': (open: boolean) => { parent.uploadOpen.value = open }, onSuccess: (id: string) => { successes++ void parent.saved(id) } }) }) const app = renderer.createApp(App) app.mount({}) await flush() const oldChild = child, pending = child.save() saved.resolve() if (change !== 'query') { parent.route.params.id = 'B' if (change === 'ABA') parent.route.params.id = 'A' } else parent.route.query.tab = 'notes' assert.equal(oldChild.props.planId, 'A', 'Vue child props have not flushed') await pending await flush() assert.equal(successes, change !== 'query' ? 0 : 1) assert.equal(oldChild.messages.length, change !== 'query' ? 0 : 1) app.unmount() parent.unmount() }) for (const reject of [false, true]) test(`cert detail same-record retry isolates old loader ${reject ? 'error' : 'success/finally'}`, async () => { const old = deferred(), fresh = deferred() let n = 0 const h = harness('plan-cert/CertDetail', { usePlPlanViewDetail: detailLoader(() => (++n === 1 ? old.promise : fresh.promise)) }) await flush() const pending = h.reload() if (reject) old.reject(new Error('old')) else old.resolve({ id: 'A', planNo: 'OLD' }) await flush() assert.equal(h.plan.value, null) assert.equal(h.loading.value, true) assert.equal(h.error.value, '') fresh.resolve({ id: 'A', planNo: 'FRESH' }) await pending assert.equal(h.plan.value.planNo, 'FRESH') h.unmount() }) test('cert detail parent sync invalidation prevents stale contract and linked callbacks', async () => { const contract = deferred(), linked = deferred() const h = harness('plan-cert/CertDetail', { usePlPlanViewDetail: detailLoader(async (id) => ({ id })), getContract: async () => ({ data: { id: 'C' } }), enrichContractReadFields: () => contract.promise, planApi: { list: () => linked.promise } }) await flush() const pending = h.openContract('C') await flush() contract.resolve({ contract: { id: 'C' }, warnings: [] }) await flush() h.invalidateRecord() linked.resolve({ data: { list: [{ id: 'OLD' }], total: 1 } }) await pending assert.equal(h.contractOpen.value, false) assert.deepEqual(h.linkedPlans.value, []) assert.equal(h.linkedLoading.value, false) h.unmount() }) test('transport local tabs retain single-line 44px targets and bordered content fits available width', () => { const sfc = parse( readFileSync(new URL('../src/views/m/pl/plan/detail.vue', import.meta.url), 'utf8') ).descriptor const style = sfc.styles.find((s) => s.scoped)!.content assert.match( style, /\.mp-plan-detail-page\.mp-h5-detail-page[\s\S]*?:deep\(\.van-tab\)\s*\{[^}]*flex:\s*0 0 auto !important/ ) assert.match(style, /:deep\(\.van-tab__text\)\s*\{[^}]*white-space:\s*nowrap/) assert.match( style, /:deep\(\.van-tabs__nav\)\s*\{[^}]*height:\s*44px[^}]*box-sizing:\s*border-box/ ) assert.match(style, /\.mp-wv-section\s*\{[^}]*min-width:\s*0[^}]*box-sizing:\s*border-box/) }) for (const component of ['CertDetail', 'CertUpload']) test(`${component}: real parent same-tick ABA remounts, unrelated query preserves instance`, async () => { let child: any, mounts = 0, reads = 0 const Child = vue.defineComponent({ props: ['planId', 'modelValue', 'permitted'], setup(props, context) { mounts++ const read = async (id: string) => { reads++ return { id, products: [{ id: 'p', isToxic: 1 }] } } child = harness(`plan-cert/${component}`, { defineProps: () => props, defineExpose: context.expose, onBeforeUnmount: vue.onBeforeUnmount, usePlPlanViewDetail: detailLoader(read, true), planApi: { get: read } }) return () => vue.h('child') } }) const renderer = vue.createRenderer({ insert(el: any, parent: any) { el.parent = parent }, remove() {}, createElement: () => ({}), createText: () => ({}), createComment: () => ({}), setText() {}, setElementText() {}, parentNode: (n: any) => n.parent, nextSibling: () => null, patchProp() {} }) const parent = harness('plan-cert/detail') await parent.init() parent.uploadOpen.value = true const App = vue.defineComponent({ setup: () => () => vue.h(Child, { key: recordChildKey(parent, component), ref: component === 'CertDetail' ? parent.detailRef : parent.uploadRef, planId: parent.id.value, modelValue: parent.uploadOpen.value, permitted: parent.canUpload.value, 'data-tab': parent.route.query.tab }) }) const app = renderer.createApp(App) app.mount({}) await flush() assert.equal(child.plan.value.id, 'A') const first = child if (component === 'CertUpload') child.products.value[0].purchaseCertUrl = 'preserved' parent.route.query.tab = 'info' await flush() assert.equal(child, first) assert.equal(reads, 1) assert.equal(mounts, 1) if (component === 'CertUpload') assert.equal(child.products.value[0].purchaseCertUrl, 'preserved') parent.route.params.id = 'B' parent.route.params.id = 'A' await flush() assert.equal(mounts, 2, 'an invalidated child must never be reused for same-tick ABA') if (component === 'CertUpload') { parent.openUpload({ id: 'A' }) await flush() } assert.equal(reads, 2) assert.equal(child.plan.value.id, 'A') assert.equal(child.loading.value, false) app.unmount() parent.unmount() }) for (const reject of [false, true]) test(`cert detail reload resets copy busy while obsolete ${reject ? 'failure' : 'success'} preserves new copy`, async () => { const old = deferred(), fresh = deferred() const h = harness('plan-cert/CertDetail', { usePlPlanViewDetail: detailLoader(async (id) => ({ id })) }) await flush() const first = h.copyAction(() => old.promise) assert.equal(h.copying.value, true) const reload = h.reload() assert.equal(h.copying.value, false, 'new load must synchronously reset previous copy busy') await reload const second = h.copyAction(() => fresh.promise) assert.equal(h.copying.value, true) if (reject) old.reject(new Error('old copy')) else old.resolve() await first assert.equal(h.copying.value, true, 'obsolete copy must not clear new copy busy') assert.deepEqual(h.messages, []) fresh.resolve() await second assert.equal(h.copying.value, false) h.unmount() }) test('plan detail identity switch invalidates old reads and closes every record action', async () => { const older = deferred() const user = vue.reactive({ accessToken: 'old', info: { id: 'old-user' } }) let reads = 0 const h = harness('plan/detail', { useUserStore: () => user, planApi: { get: () => (++reads === 1 ? older.promise : Promise.resolve({ id: 'A', marker: 'new' })) }, loadMobileDetailReads: async () => ({ errors: [] }) }) h.ready() await flush() h.moreOpen.value = h.voidOpen.value = h.vehicleOpen.value = true user.accessToken = 'new' assert.equal(h.moreOpen.value, false) assert.equal(h.voidOpen.value, false) assert.equal(h.vehicleOpen.value, false) await flush() older.resolve({ id: 'A', marker: 'old' }) await flush() assert.equal(h.plan.value.marker, 'new') assert.equal(reads, 2) user.accessToken = '' assert.equal(h.plan.value, null) assert.equal(h.loading.value, false) assert.match(h.errorText.value, /登录状态/) await h.runAction('wait') assert.deepEqual(h.writes, []) h.unmount() }) for (const change of ['token', 'user', 'logout']) test(`cert parent ${change} change invalidates children and pending ready confirmation`, async () => { const user = vue.reactive({ accessToken: 'old', info: { id: 'old-user' } }) const confirm = deferred() let permissions = 0, invalidations = 0 const h = harness('plan-cert/detail', { useUserStore: () => user, showCrmConfirmDialog: () => confirm.promise, fetchGetMenuCodes: async () => (++permissions === 1 ? { all: true } : { menuCodes: [] }) }) await h.init() h.detailRef.value = h.uploadRef.value = { invalidateRecord: () => invalidations++ } h.uploadOpen.value = true const pending = h.setReady({ id: 'A' }, true) await flush() const epoch = h.recordEpoch.value if (change === 'token') user.accessToken = 'new' else if (change === 'user') user.info = { id: 'new-user' } else user.accessToken = '' assert.equal(h.uploadOpen.value, false) assert.equal(h.canReady.value, false) assert.ok(h.recordEpoch.value > epoch) assert.ok(invalidations >= 2) confirm.resolve() await pending await flush() assert.deepEqual(h.writes, []) assert.equal(h.canUpload.value, false) assert.equal(permissions, change === 'logout' ? 1 : 2) h.unmount() }) test('cert parent old permission read cannot grant access after identity changes', async () => { const user = vue.reactive({ accessToken: 'old', info: { id: 'user' } }) const old = deferred() let reads = 0 const h = harness('plan-cert/detail', { useUserStore: () => user, fetchGetMenuCodes: () => (++reads === 1 ? old.promise : Promise.resolve({ menuCodes: [] })) }) const init = h.init() user.accessToken = 'new' await flush() old.resolve({ all: true }) await init assert.equal(h.canUpload.value, false) assert.equal(h.canReady.value, false) assert.equal(h.canPrice.value, false) assert.equal(reads, 2) h.unmount() }) test('cert detail requests mobile metadata once and passes that snapshot to the shared read loader', async () => { const snapshot = { id: 'A', products: [], deliveries: [] } const reads: any[] = [], snapshots: any[] = [] const loader = detailLoader(async () => snapshot) const h = harness('plan-cert/CertDetail', { planApi: { get: async (...args: any[]) => { reads.push(args) return { data: snapshot } } }, usePlPlanViewDetail: () => { const detail = loader() const load = detail.loadDetail detail.loadDetail = (id: string, options: any) => { snapshots.push(options?.initialPlan) return load(id) } return detail } }) await flush() assert.deepEqual(reads, [['A', true]]) assert.equal(snapshots[0], snapshot) assert.equal(h.plan.value.id, 'A') h.unmount() })