172 lines
6.4 KiB
TypeScript
172 lines
6.4 KiB
TypeScript
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,
|
|
[]
|
|
)
|
|
}
|
|
})
|