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 = async () => []) { const events: any[] = [] const b: Record = { 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, [] ) } })