| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import test from 'node:test'
- import { ref } from 'vue'
- import { parse } from '@vue/compiler-sfc'
- import ts from 'typescript'
- import { apiTime, unwrapRows } from '../src/views/m/crm/customer/model'
- const file = new URL('../src/views/m/crm/customer/FollowPanel.vue', import.meta.url)
- const { descriptor } = parse(readFileSync(file, 'utf8'))
- const ast = ts.createSourceFile(
- 'FollowPanel.ts',
- descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const states = [
- 'rows',
- 'contacts',
- 'loading',
- 'acting',
- 'editing',
- 'error',
- 'saveError',
- 'form',
- 'upload'
- ]
- const methods = ['open', 'save', 'load']
- const source = ast.statements
- .filter((node) => {
- if (ts.isVariableStatement(node)) {
- return states.includes(node.declarationList.declarations[0].name.getText(ast))
- }
- return (
- ts.isFunctionDeclaration(node) &&
- [...methods, 'showSaveError'].includes(node.name?.text || '')
- )
- })
- .map((node) => node.getText(ast))
- .join('\n')
- // Execute the real component state, open/save/load functions and date formatter.
- // Only transport and the attachment component are stubbed; no API or browser is used.
- function harness(create: () => Promise<unknown> = async () => undefined) {
- const payloads: Record<string, any>[] = []
- const events: string[] = []
- const notices: string[] = []
- let reloads = 0
- const bindings = {
- ref,
- // This payload fixture stays on one customer; session transitions are covered by renderer tests.
- identity: {
- newSession() {},
- begin: () => ({ id: 'customer-test', valid: () => true })
- },
- apiTime,
- unwrapRows,
- showFailToast: (message: string) => notices.push(message),
- props: { customerId: 'customer-test' },
- emit: (event: string) => events.push(event),
- fetchContactByCustomerId: async () => [],
- fetchFollowByCustomerId: async () => {
- reloads++
- return [{ id: 'saved-follow' }]
- },
- fetchCreateCustomerFollow: async (payload: Record<string, any>) => {
- payloads.push(payload)
- return create()
- }
- }
- const code = ts.transpile(source, { target: ts.ScriptTarget.ESNext })
- const instance = new Function(
- ...Object.keys(bindings),
- `${code}\nreturn { ${[...states, ...methods].join(', ')} }`
- )(...Object.values(bindings))
- return { ...instance, payloads, events, notices, reloads: () => reloads }
- }
- async function draft(h: ReturnType<typeof harness>) {
- await h.open()
- Object.assign(h.form.value, {
- followContent: '联系内容',
- attachment: 'fixture.pdf',
- remark: '保留备注'
- })
- }
- test('an omitted optional next contact date is absent from the serialized save payload', async () => {
- const h = harness()
- await draft(h)
- assert.equal(h.form.value.nextTime, '')
- await h.save()
- assert.equal(h.payloads.length, 1)
- assert.equal(h.payloads[0].nextTime, undefined)
- assert.equal(Object.hasOwn(JSON.parse(JSON.stringify(h.payloads[0])), 'nextTime'), false)
- assert.equal(h.payloads[0].customerId, 'customer-test')
- assert.equal(h.payloads[0].attachment, 'fixture.pdf')
- assert.equal(h.editing.value, false)
- assert.equal(h.acting.value, false)
- assert.deepEqual(h.events, ['changed'])
- assert.equal(h.reloads(), 1)
- assert.equal(h.rows.value[0].id, 'saved-follow')
- })
- test('null and missing optional next contact dates never serialize as empty strings', async () => {
- for (const value of [null, undefined]) {
- const h = harness()
- await draft(h)
- h.form.value.nextTime = value
- await h.save()
- assert.equal(h.payloads[0].nextTime, undefined)
- assert.equal(Object.hasOwn(JSON.parse(JSON.stringify(h.payloads[0])), 'nextTime'), false)
- }
- })
- test('a valid local date retains the selected time and gains API seconds only when needed', async () => {
- for (const [input, expected] of [
- ['2026-10-22T09:30', '2026-10-22 09:30:00'],
- ['2026-10-22T09:30:45', '2026-10-22 09:30:45']
- ]) {
- const h = harness()
- await draft(h)
- h.form.value.nextTime = input
- await h.save()
- assert.equal(h.payloads[0].nextTime, expected)
- assert.equal(h.form.value.nextTime, input)
- }
- })
- test('clearing a previously selected date submits no stale date or empty datetime', async () => {
- const h = harness()
- await draft(h)
- h.form.value.nextTime = '2026-10-22T09:30'
- h.form.value.nextTime = ''
- await h.save()
- assert.equal(h.payloads[0].nextTime, undefined)
- assert.equal(h.form.value.nextTime, '')
- })
- test('an uploading attachment blocks submission and leaves the draft open', async () => {
- const h = harness()
- await draft(h)
- h.upload.value = { hasUploading: () => true }
- const before = { ...h.form.value }
- await h.save()
- assert.equal(h.payloads.length, 0)
- assert.equal(h.saveError.value, '附件上传中,请稍候')
- assert.equal(h.editing.value, true)
- assert.equal(h.acting.value, false)
- assert.deepEqual(h.form.value, before)
- assert.equal(h.reloads(), 0)
- assert.deepEqual(h.events, [])
- })
- test('a failed save retains all inputs and allows a corrected retry', async () => {
- let failure = true
- const h = harness(async () => {
- if (failure) throw new Error('fixture save failure')
- })
- await draft(h)
- h.form.value.nextTime = '2026-10-22T09:30'
- const before = { ...h.form.value }
- await h.save()
- assert.equal(h.saveError.value, 'fixture save failure')
- assert.equal(h.editing.value, true)
- assert.equal(h.acting.value, false)
- assert.deepEqual(h.form.value, before)
- assert.deepEqual(h.events, [])
- assert.equal(h.reloads(), 0)
- failure = false
- h.form.value.nextTime = ''
- await h.save()
- assert.equal(h.payloads.length, 2)
- assert.equal(h.payloads[1].nextTime, undefined)
- assert.equal(h.saveError.value, '')
- assert.equal(h.editing.value, false)
- })
- test('a pending save blocks duplicate submission until it settles', async () => {
- let resolve!: () => void
- const h = harness(
- () =>
- new Promise<void>((done) => {
- resolve = done
- })
- )
- await draft(h)
- const pending = h.save()
- assert.equal(h.acting.value, true)
- await h.save()
- assert.equal(h.payloads.length, 1)
- resolve()
- await pending
- assert.equal(h.acting.value, false)
- })
- test('required content validation still runs before transport', async () => {
- const h = harness()
- await h.open()
- h.form.value.followContent = ' '
- await h.save()
- assert.equal(h.payloads.length, 0)
- assert.equal(h.saveError.value, '跟进内容不能为空')
- assert.equal(h.editing.value, true)
- })
- test('follow save pairs the selected contact ID with its loaded canonical name', async () => {
- for (const selectedId of [42, '42']) {
- const h = harness()
- await draft(h)
- h.contacts.value = [{ id: 42, contactName: '当前联系人' }]
- h.form.value.contactId = selectedId
- h.form.value.contactName = '过期姓名'
- await h.save()
- assert.equal(h.payloads[0].contactName, '当前联系人')
- assert.equal(String(h.payloads[0].contactId), '42')
- }
- })
- test('cleared and unknown follow contacts never submit a stale name', async () => {
- for (const selectedId of ['', null, undefined, 'unknown']) {
- const h = harness()
- await draft(h)
- h.contacts.value = [{ id: 'known', contactName: '旧联系人' }]
- h.form.value.contactId = selectedId
- h.form.value.contactName = '过期姓名'
- await h.save()
- assert.equal(h.payloads[0].contactName, '')
- }
- })
- test('a cleared follow type blocks transport with visible field feedback and retains the draft', async () => {
- for (const value of ['', ' ', null, undefined]) {
- const h = harness()
- await draft(h)
- h.form.value.followType = value
- const before = { ...h.form.value }
- await h.save()
- assert.equal(h.payloads.length, 0)
- assert.equal(h.saveError.value, '跟进方式不能为空')
- assert.equal(h.notices.at(-1), h.saveError.value)
- assert.deepEqual(h.form.value, before)
- assert.equal(h.editing.value, true)
- assert.equal(h.acting.value, false)
- assert.equal(h.reloads(), 0)
- }
- })
|