| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- 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 { ref, nextTick } from 'vue'
- import ts from 'typescript'
- const file = new URL('../src/views/crm/quote/components/QuoteFormPanel.vue', import.meta.url)
- function harness(fetchContacts: (input: any) => Promise<any> = async () => []) {
- const events: any[] = []
- const b: Record<string, any> = {
- form: ref({
- customerId: 'C1',
- customerName: '客户一',
- contactId: 'P1',
- contactName: '联系人一',
- products: [{ productId: 'product' }],
- freight: 12
- }),
- isPageVariant: ref(true),
- route: { fullPath: '/m/crm/quote/new' },
- customerShortName: ref('简称'),
- confirmedCustomerId: ref('C1'),
- contactOptions: ref([]),
- contactOptionsLoading: ref(false),
- contactOptionsError: ref(''),
- contactEmptyPrompted: ref(false),
- opportunityDisplayName: ref(''),
- mobileContactFormCustomerId: ref(''),
- mobileContactKeyword: ref(''),
- contactSelectorVisible: ref(false),
- mobileCustomerEditor: ref(null),
- mobileInvoiceCustomer: ref(null),
- customerSelectorVisible: ref(true),
- fetchContactsByCustomerId: fetchContacts,
- extractContactList: (res: any) => res?.data ?? res,
- ensureContactOption: () => {},
- ensureCustomerOption: (row: any) => events.push(['option', row]),
- updateQuoteTitleAndNo: () => events.push(['title']),
- loadMobileCustomers: async () => events.push(['reload']),
- basicLabels: ref({ contactName: '客户联系人' }),
- showFailToast: (message: string) => events.push(['error', message]),
- ElMessage: { warning: () => {} },
- nextTick,
- contactFormRef: ref({ open: () => events.push(['web-contact']) }),
- $modal: { msgWarning: () => {} },
- handleContactConfirm: (row: any) => {
- b.form.value.contactId = row.id
- b.form.value.contactName = row.contactName
- }
- }
- const names = [
- 'contactOptionsRequest',
- 'loadContactOptions',
- 'openMobileContactForm',
- 'openContactFormDialog',
- 'handleCustomerConfirm',
- 'openMobileContact',
- 'handleMobileContactSaved',
- 'handleMobileCustomerSaved',
- 'openMobileCustomerEditor',
- 'openMobileInvoice'
- ]
- const ast = ts.createSourceFile(
- 'panel.ts',
- parse(readFileSync(file, 'utf8')).descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const nodes = ast.statements.filter((n) =>
- ts.isFunctionDeclaration(n)
- ? names.includes(n.name?.text || '')
- : ts.isVariableStatement(n) &&
- names.includes(n.declarationList.declarations[0].name.getText(ast))
- )
- assert.equal(nodes.length, names.length, 'maintenance handlers must exist')
- const api = new Function(
- ...Object.keys(b),
- ts.transpile(nodes.map((n) => n.getText(ast)).join('\n')) + `;return {${names.join(',')}}`
- )(...Object.values(b))
- return { ...b, ...api, events }
- }
- test('customer chooser offers create, edit and invoice actions without selecting the row', () => {
- const t = parse(readFileSync(file, 'utf8')).descriptor.template!.content
- for (const action of [
- '@click="openMobileCustomerEditor()"',
- '@click.stop="openMobileCustomerEditor(item)"',
- '@click.stop="openMobileInvoice(item)"'
- ])
- assert.ok(t.includes(action), action)
- })
- test('empty contacts open mobile creation; existing primary contact is selected', async () => {
- for (const contacts of [
- [],
- [
- { id: 'P2', contactName: '普通' },
- { id: 'P3', contactName: '主要', isPrimary: 1 }
- ]
- ]) {
- const h = harness(async () => contacts)
- await h.handleCustomerConfirm({ id: 'C2', customerName: '客户二', shortName: '二' })
- assert.equal(h.mobileContactFormCustomerId.value, contacts.length ? '' : 'C2')
- assert.equal(h.form.value.contactId, contacts.length ? 'P3' : undefined)
- assert.equal(h.form.value.freight, 12)
- }
- })
- test('contact load failure permits retry and never opens create', async () => {
- let fail = true
- const h = harness(async () => {
- if (fail) throw new Error('网络失败')
- return []
- })
- await h.handleCustomerConfirm({ id: 'C2', customerName: '客户二' })
- assert.equal(h.mobileContactFormCustomerId.value, '')
- assert.ok(h.contactOptionsError.value)
- fail = false
- await h.openMobileContact()
- assert.equal(h.mobileContactFormCustomerId.value, 'C2')
- })
- for (const change of ['customer', 'route'])
- test(`late contact responses after ${change} change are discarded`, async () => {
- let resolve!: (value: any) => void
- const h = harness(
- () =>
- new Promise((r) => {
- resolve = r
- })
- )
- const pending = h.handleCustomerConfirm({ id: 'C2', customerName: '客户二' })
- if (change === 'customer') h.form.value.customerId = 'C3'
- else h.route.fullPath = '/m/crm/quote'
- resolve([])
- await pending
- assert.equal(h.mobileContactFormCustomerId.value, '')
- })
- test('customer editing refreshes labels but preserves quotation and contact', async () => {
- const h = harness()
- await h.handleMobileCustomerSaved({ id: 'C1', customerName: '新名称', shortName: '新简称' })
- assert.equal(h.form.value.customerName, '新名称')
- assert.equal(h.customerShortName.value, '新简称')
- assert.equal(h.form.value.contactId, 'P1')
- assert.equal(h.form.value.products[0].productId, 'product')
- assert.equal(h.form.value.freight, 12)
- assert.ok(h.events.some((e: any) => e[0] === 'reload'))
- })
- test('created contact fills current customer; stale customer events are ignored', async () => {
- const h = harness()
- h.mobileContactFormCustomerId.value = 'C1'
- await h.handleMobileContactSaved({ id: 'P2', customerId: 'C1', contactName: '新增联系人' })
- assert.equal(h.form.value.contactId, 'P2')
- await h.handleMobileContactSaved({ id: 'P3', customerId: 'OTHER', contactName: '错误联系人' })
- assert.equal(h.form.value.contactId, 'P2')
- })
- test('affected Vue templates compile', () => {
- for (const path of [
- 'views/crm/quote/components/QuoteFormPanel.vue',
- 'views/m/crm/customer/form.vue',
- 'views/m/crm/customer/ContactPanel.vue'
- ]) {
- const d = parse(readFileSync(new URL('../src/' + path, import.meta.url), 'utf8')).descriptor
- const s = compileScript(d, { id: path })
- assert.deepEqual(
- compileTemplate({
- source: d.template!.content,
- filename: path,
- id: path,
- compilerOptions: { bindingMetadata: s.bindings }
- }).errors,
- []
- )
- }
- })
|