| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import test from 'node:test'
- import { computed, ref } from 'vue'
- import { parse } from '@vue/compiler-sfc'
- import ts from 'typescript'
- import { getMpQuoteAmountSummary } from '../src/utils/mp-crm-quote-card'
- function component(path: string) {
- return parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor
- }
- const formDescriptor = component('../src/views/crm/quote/components/QuoteFormPanel.vue')
- const detailDescriptor = component('../src/views/m/crm/quote/detail.vue')
- // Execute actual production declarations. Only transport/UI side effects are stubbed.
- function declarations(source: string, names: string[], bindings: Record<string, any>) {
- const ast = ts.createSourceFile('component.ts', source, ts.ScriptTarget.Latest, true)
- const selected = ast.statements.filter((node) =>
- ts.isVariableStatement(node)
- ? names.includes(node.declarationList.declarations[0].name.getText(ast))
- : ts.isFunctionDeclaration(node) && names.includes(node.name?.text || '')
- )
- const code = ts
- .transpile(
- selected
- .map((node) => node.getText(ast))
- .join('\n')
- .replace(/export /g, ''),
- {
- target: ts.ScriptTarget.ESNext,
- module: ts.ModuleKind.None
- }
- )
- .replace(/export /g, '')
- const found = names.filter((name) =>
- selected.some((node) =>
- ts.isVariableStatement(node)
- ? node.declarationList.declarations[0].name.getText(ast) === name
- : (node as ts.FunctionDeclaration).name?.text === name
- )
- )
- return new Function(...Object.keys(bindings), `${code}\nreturn { ${found.join(', ')} }`)(
- ...Object.values(bindings)
- )
- }
- function formHarness(page: boolean, prepare: () => boolean | void = () => {}) {
- const calls: Array<{ kind: string; payload: any }> = []
- let validation: Promise<void> | undefined
- const form = ref({
- id: 'quote-1',
- customerId: 'customer-new',
- contactId: 'contact-new',
- contactName: '新姓名',
- finalAmount: 25,
- freight: 5,
- barrelDeposit: 3,
- products: [
- {
- productId: 'product-1',
- productName: '产品',
- quantity: 2,
- price: 12.5,
- subtotal: 25,
- packageSpec: '桶',
- deliveryMethod: '送货'
- }
- ]
- })
- const transport = (kind: string) => async (payload: any) => {
- calls.push({ kind, payload: JSON.parse(JSON.stringify(payload)) })
- return {}
- }
- const bindings = {
- ref,
- computed,
- form,
- isPageVariant: ref(page),
- formRef: ref({
- validate: (fn: (valid: boolean) => Promise<void>) => {
- validation = fn(true)
- }
- }),
- submitFormRef: ref({ validate: async () => true }),
- submitForm: ref({ approverIds: ['approver-1'], remark: '' }),
- submitDialog: ref({ loading: false, visible: true }),
- saveLoading: ref(false),
- attachmentList: ref([]),
- pageMissingMessage: () => '',
- syncQuoteProductsForSubmit: prepare,
- showFailToast: () => {},
- ElMessage: { warning: () => {} },
- $modal: { msgSuccess: () => {}, msgError: () => {}, msgWarning: () => {} },
- updateQuote: transport('web-update'),
- updateMobileQuote: transport('mobile-update'),
- submitQuote: transport('submit'),
- addQuote: transport('add'),
- finishSuccess: () => {},
- updateQuoteTitleAndNo: () => {}
- }
- const api = declarations(
- formDescriptor.scriptSetup!.content,
- ['calculatedAmount', 'mobileFinalAmount', 'handleSaveDraft', 'handleConfirmApproval'],
- bindings
- )
- return { ...bindings, ...api, calls, saved: () => validation }
- }
- for (const page of [true, false]) {
- test(`${page ? 'mobile' : 'desktop'} draft save dispatches through its own endpoint`, async () => {
- const h = formHarness(page)
- h.handleSaveDraft()
- await h.saved()
- assert.deepEqual(
- h.calls.map((call) => call.kind),
- [page ? 'mobile-update' : 'web-update']
- )
- assert.equal(h.calls[0].payload.finalAmount, 25)
- assert.equal(h.calls[0].payload.contactId, 'contact-new')
- })
- test(`${page ? 'mobile' : 'desktop'} approval saves via its own endpoint before submitting`, async () => {
- const h = formHarness(page)
- await h.handleConfirmApproval()
- assert.deepEqual(
- h.calls.map((call) => call.kind),
- [page ? 'mobile-update' : 'web-update', 'submit']
- )
- assert.equal(h.calls[0].payload.finalAmount, 25)
- assert.equal(h.calls[1].payload.id, 'quote-1')
- })
- }
- test('mobile amount includes freight and deposit while desktop and payload keep product amount', () => {
- const h = formHarness(true)
- // Evaluate the actual mobile display expression, so the baseline displays 25 and fails.
- const mobileExpression = formDescriptor.template!.content.match(
- /<strong class="money-display">¥\{\{ (.*?) \}\}/
- )![1]
- const display = new Function(
- 'calculatedAmount',
- 'mobileFinalAmount',
- `return ${mobileExpression}`
- )(h.calculatedAmount.value, h.mobileFinalAmount?.value)
- assert.equal(display, '33.00')
- assert.equal(h.calculatedAmount.value.finalAmount, '25.00')
- assert.equal(h.form.value.finalAmount, 25)
- assert.match(
- formDescriptor.template!.content,
- /<span class="money-display" style="font-size: 20px">¥\{\{ calculatedAmount.finalAmount \}\}/
- )
- h.form.value.freight = 0
- h.form.value.barrelDeposit = 0
- assert.equal(h.mobileFinalAmount.value, '25.00')
- })
- test('mobile form display agrees with the detail amount and avoids adding fees to stored finalAmount', () => {
- const h = formHarness(true)
- const detail = declarations(detailDescriptor.scriptSetup!.content, ['amountSummary'], {
- computed,
- getMpQuoteAmountSummary,
- quote: ref({ totalAmount: 25, finalAmount: 25, freight: 5, barrelDeposit: 3 })
- })
- assert.equal(Number(h.mobileFinalAmount?.value), detail.amountSummary.value.finalAmount)
- })
- test('mobile API normalizes contact IDs without changing the original desktop serializer', async () => {
- const source = readFileSync(new URL('../src/api/crm/quote/index.ts', import.meta.url), 'utf8')
- const calls: any[] = []
- const api = declarations(source, ['stringifyBody', 'updateQuote', 'updateMobileQuote'], {
- BASE: '/web/crm/quote',
- request: { post: async (payload: any) => calls.push(payload) }
- })
- assert.equal(typeof api.updateMobileQuote, 'function', 'mobile API wrapper is missing')
- for (const id of [42, '9007199254740993', '', null, undefined]) {
- const input = { id: 1, customerId: 2, contactId: id }
- await api.updateMobileQuote(input)
- assert.equal(calls.at(-1).url, '/web/crm/quote/mobile-update')
- assert.equal(calls.at(-1).data.contactId, id == null ? id : String(id))
- assert.equal(input.contactId, id)
- }
- await api.updateQuote({ id: 1, customerId: 2, contactId: 42 })
- assert.equal(calls.at(-1).url, '/web/crm/quote/update')
- assert.equal(calls.at(-1).data.contactId, 42)
- })
- test('rejection confirmation describes editing back to draft and retains approval behavior', async () => {
- const dialogs: any[] = []
- const requests: any[] = []
- const bindings = {
- // This confirmation test operates on a valid current approval session.
- captureOperation: () => ({ id: 'quote-1', epoch: 1 }),
- isCurrentOperation: () => true,
- disposed: false,
- operationEpoch: 1,
- pendingApproval: ref({ id: 'approval-1' }),
- approveResult: ref('rejected'),
- approveOpinion: ref(' 请补充资料 '),
- acting: ref(false),
- approveOpen: ref(true),
- showFailToast: () => {},
- showSuccessToast: () => {},
- loadDetail: async () => {},
- showCrmConfirmDialog: async (dialog: any) => {
- dialogs.push(dialog)
- },
- approveQuote: async (payload: any) => {
- requests.push(payload)
- }
- }
- const h = declarations(detailDescriptor.scriptSetup!.content, ['confirmApprove'], bindings)
- await h.confirmApprove()
- assert.doesNotMatch(dialogs[0].message, /无法再次提交审批/)
- assert.match(dialogs[0].message, /编辑.*草稿.*重新提交审批/)
- assert.deepEqual(requests, [
- { id: 'approval-1', approvalResult: 'rejected', approvalOpinion: '请补充资料' }
- ])
- assert.equal(bindings.acting.value, false)
- assert.equal(bindings.approveOpen.value, false)
- })
- for (const action of ['draft', 'approval']) {
- test(`mobile ${action} refuses writes when metadata preparation is blocked`, async () => {
- const h = formHarness(true, () => false)
- if (action === 'draft') {
- h.handleSaveDraft()
- await h.saved()
- } else await h.handleConfirmApproval()
- assert.equal(h.calls.length, 0)
- })
- }
|