634 lines
21 KiB
TypeScript
634 lines
21 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import test from 'node:test'
|
|
import { computed, ref } from 'vue'
|
|
import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc'
|
|
import ts from 'typescript'
|
|
import * as model from '../src/views/m/crm/customer/model'
|
|
import * as validation from '../src/views/m/crm/customer/validation'
|
|
|
|
const base = new URL('../src/views/m/crm/customer/', import.meta.url)
|
|
|
|
test('customer and contact native submissions reach the same explicit validation as save buttons', () => {
|
|
for (const file of ['form.vue', 'ContactPanel.vue']) {
|
|
const { descriptor } = parse(readFileSync(new URL(file, base), 'utf8'))
|
|
assert.match(
|
|
descriptor.template!.content,
|
|
/<form\b[^>]*\bnovalidate\b[^>]*@submit\.prevent="save"/
|
|
)
|
|
}
|
|
})
|
|
|
|
// Execute the current production declarations; stub only transport, navigation and UI feedback.
|
|
function execute(file: string, names: string[], bindings: Record<string, any>) {
|
|
// Validation fixtures keep one record/session; identity transitions have renderer coverage separately.
|
|
bindings = {
|
|
identity: {
|
|
begin: () => ({ id: String(bindings.props?.customerId || bindings.props?.opportunity?.id || ''), valid: () => true })
|
|
},
|
|
...validation,
|
|
...bindings
|
|
}
|
|
const { descriptor } = parse(readFileSync(new URL(file, base), 'utf8'))
|
|
const ast = ts.createSourceFile(
|
|
file,
|
|
descriptor.scriptSetup!.content,
|
|
ts.ScriptTarget.Latest,
|
|
true
|
|
)
|
|
const selected = [...names, 'showSaveError']
|
|
const source = ast.statements
|
|
.filter((node) =>
|
|
ts.isFunctionDeclaration(node)
|
|
? selected.includes(node.name?.text || '')
|
|
: ts.isVariableStatement(node) &&
|
|
selected.includes(node.declarationList.declarations[0].name.getText(ast))
|
|
)
|
|
.map((node) => node.getText(ast))
|
|
.join('\n')
|
|
return new Function(
|
|
...Object.keys(bindings),
|
|
`${ts.transpile(source, { target: ts.ScriptTarget.ESNext })}\nreturn { ${names.join(',')} }`
|
|
)(...Object.values(bindings))
|
|
}
|
|
|
|
function feedback() {
|
|
const notices: string[] = []
|
|
const events: string[] = []
|
|
return {
|
|
notices,
|
|
events,
|
|
showFailToast: (message: string) => notices.push(message),
|
|
emit: (event: string) => events.push(event)
|
|
}
|
|
}
|
|
|
|
function contactHarness(edit: boolean, patch: model.Row = {}, write = async () => {}) {
|
|
const payloads: model.Row[] = []
|
|
const routes: string[] = []
|
|
let reloads = 0
|
|
const bindings = {
|
|
...model,
|
|
...feedback(),
|
|
acting: ref(false),
|
|
saveError: ref(''),
|
|
editing: ref(true),
|
|
form: ref({
|
|
contactName: '测试联系人',
|
|
mobile: '13800138000',
|
|
telephone: '前台转 123 / 工作日',
|
|
email: '',
|
|
isKey: 1,
|
|
isPrimary: 0,
|
|
remark: '保留草稿',
|
|
...(edit ? { id: 'contact-1' } : {}),
|
|
...patch
|
|
}),
|
|
props: { customerId: 'customer-1' },
|
|
fetchCreateContact: async (data: model.Row) => {
|
|
payloads.push(data)
|
|
routes.push('create')
|
|
await write()
|
|
},
|
|
fetchUpdateContact: async (data: model.Row) => {
|
|
payloads.push(data)
|
|
routes.push('update')
|
|
await write()
|
|
},
|
|
load: async () => {
|
|
reloads++
|
|
}
|
|
}
|
|
return {
|
|
...bindings,
|
|
...execute('ContactPanel.vue', ['save'], bindings),
|
|
payloads,
|
|
routes,
|
|
reloads: () => reloads
|
|
}
|
|
}
|
|
|
|
for (const edit of [false, true]) {
|
|
for (const [key, value, message] of [
|
|
['contactName', ' ', '联系人姓名'],
|
|
['mobile', '', '手机号'],
|
|
['mobile', '123', '手机号'],
|
|
['mobile', '12800138000', '手机号'],
|
|
['mobile', '138001380000', '手机号'],
|
|
['email', 'bad', '邮箱'],
|
|
['email', 'a@b', '邮箱']
|
|
]) {
|
|
test(`contact ${edit ? 'edit' : 'create'} rejects ${key}=${value} before transport`, async () => {
|
|
const h = contactHarness(edit, { [key]: value })
|
|
const before = { ...h.form.value }
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.match(h.saveError.value, new RegExp(message))
|
|
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)
|
|
assert.deepEqual(h.events, [])
|
|
})
|
|
}
|
|
for (const email of ['', null, undefined, 'a@example.com', 'first.last+tag@sub.example.co.uk']) {
|
|
test(`contact ${edit ? 'edit' : 'create'} accepts optional email ${email} and free-form telephone`, async () => {
|
|
const h = contactHarness(edit, { email })
|
|
await h.save()
|
|
assert.deepEqual(h.routes, [edit ? 'update' : 'create'])
|
|
assert.equal(h.payloads[0].telephone, '前台转 123 / 工作日')
|
|
assert.equal(h.payloads[0].email, email ?? '')
|
|
assert.equal(h.payloads[0].customerId, 'customer-1')
|
|
assert.equal(h.payloads[0].isKey, 1)
|
|
assert.equal(h.payloads[0].id, edit ? 'contact-1' : undefined)
|
|
assert.equal(h.editing.value, false)
|
|
assert.equal(h.reloads(), 1)
|
|
assert.deepEqual(h.events, ['changed'])
|
|
})
|
|
}
|
|
}
|
|
|
|
function customerHarness(edit = false, patch: model.Row = {}, actions: model.Row = {}) {
|
|
const calls: string[] = []
|
|
const payloads: model.Row[] = []
|
|
const navigations: string[] = []
|
|
const bindings = {
|
|
...model,
|
|
...feedback(),
|
|
saving: ref(false),
|
|
error: ref(''),
|
|
uploads: new Map(),
|
|
id: ref(edit ? 'customer-1' : ''),
|
|
state: { customerId: '' },
|
|
invoice: ref<model.Row>({}),
|
|
form: ref(
|
|
model.customerForm({
|
|
...(edit ? { id: 'customer-1' } : {}),
|
|
customerName: '测试客户',
|
|
shortName: '测试',
|
|
customerType: 'type',
|
|
source: 'source',
|
|
parkArea: '新园区',
|
|
province: '省',
|
|
city: '市',
|
|
district: '区',
|
|
mobile: '',
|
|
email: '',
|
|
telephone: '办公总机 / 分机 123',
|
|
remark: '保留草稿',
|
|
...patch
|
|
})
|
|
),
|
|
dict: {
|
|
getOptions: () => [],
|
|
ensure: async () => {
|
|
calls.push('ensure')
|
|
return { value: '新园区' }
|
|
}
|
|
},
|
|
showCrmConfirmDialog: async () => {
|
|
calls.push('confirm')
|
|
},
|
|
fetchCheckDuplicate: async (data: model.Row) => {
|
|
calls.push('duplicate')
|
|
return actions.duplicate ? actions.duplicate(data) : { isDuplicate: false }
|
|
},
|
|
fetchCreateCustomer: async (data: model.Row) => {
|
|
calls.push('create')
|
|
payloads.push(data)
|
|
if (actions.write) await actions.write()
|
|
return { id: 'created-customer' }
|
|
},
|
|
fetchUpdateCustomer: async (data: model.Row) => {
|
|
calls.push('update')
|
|
payloads.push(data)
|
|
if (actions.write) await actions.write()
|
|
},
|
|
addInvoiceInfo: async (data: model.Row) => {
|
|
calls.push('invoice')
|
|
payloads.push(data)
|
|
if (actions.invoice) await actions.invoice()
|
|
},
|
|
showSuccessToast: () => {},
|
|
router: {
|
|
replace: async (path: string) => {
|
|
navigations.push(path)
|
|
}
|
|
}
|
|
}
|
|
return { ...bindings, ...execute('form.vue', ['save'], bindings), calls, payloads, navigations }
|
|
}
|
|
|
|
test('production customer and contact email validation agrees with the Web async-validator public API', async () => {
|
|
const require = createRequire(import.meta.url)
|
|
const requireFromElement = createRequire(require.resolve('element-plus/package.json'))
|
|
const Schema = requireFromElement('async-validator').default
|
|
const schema = new Schema({ email: { type: 'email' } })
|
|
for (const email of [
|
|
'',
|
|
null,
|
|
undefined,
|
|
'bad',
|
|
'a@b',
|
|
'a@example.com',
|
|
'first.last+tag@sub.example.co.uk',
|
|
'a..b@example.com',
|
|
'.a@example.com',
|
|
'a@example.c',
|
|
' a@example.com ',
|
|
' ',
|
|
'"quoted name"@example.com',
|
|
'user@[192.168.1.1]',
|
|
'名字@例子.中国',
|
|
'a@ex-ample.com',
|
|
'a@foo_bar.com',
|
|
'a@domain.123',
|
|
123,
|
|
'a'.repeat(307) + '@example.com',
|
|
'a'.repeat(309) + '@example.com'
|
|
]) {
|
|
const valid = await schema.validate({ email }, { suppressWarning: true }).then(
|
|
() => true,
|
|
() => false
|
|
)
|
|
for (const h of [contactHarness(false, { email }), customerHarness(false, { email })]) {
|
|
await h.save()
|
|
assert.equal(h.payloads.length, valid ? 1 : 0, `email=${String(email)}`)
|
|
if (!valid) assert.match(h.notices.at(-1) || '', /邮箱/)
|
|
}
|
|
}
|
|
})
|
|
|
|
for (const edit of [false, true]) {
|
|
const invalid = [
|
|
...model.customerGroups
|
|
.flatMap((g) => g.fields)
|
|
.filter((f) => f.required)
|
|
.map((f) => [f.key, ' ', f.label]),
|
|
['mobile', '123', '手机号'],
|
|
['email', 'bad', '邮箱'],
|
|
['email', 'a@b', '邮箱']
|
|
]
|
|
for (const [key, value, label] of invalid) {
|
|
test(`customer ${edit ? 'edit' : 'create'} rejects ${key} before dictionary, duplicate check or write`, async () => {
|
|
const h = customerHarness(edit, { [key]: value })
|
|
const before = { ...h.form.value }
|
|
await h.save()
|
|
assert.deepEqual(h.calls, [])
|
|
assert.match(h.error.value, new RegExp(label))
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.deepEqual(h.form.value, before)
|
|
assert.equal(h.saving.value, false)
|
|
assert.deepEqual(h.navigations, [])
|
|
})
|
|
}
|
|
for (const patch of [
|
|
{ mobile: '', email: '' },
|
|
{ mobile: null, email: undefined },
|
|
{ mobile: '19900138000', email: 'a@example.com' }
|
|
]) {
|
|
test(`customer ${edit ? 'edit' : 'create'} accepts optional contact details ${JSON.stringify(patch)}`, async () => {
|
|
const h = customerHarness(edit, patch)
|
|
await h.save()
|
|
assert.deepEqual(h.calls, ['confirm', 'ensure', 'duplicate', edit ? 'update' : 'create'])
|
|
assert.equal(h.payloads[0].telephone, '办公总机 / 分机 123')
|
|
assert.equal(h.navigations.length, 1)
|
|
assert.equal(h.error.value, '')
|
|
})
|
|
}
|
|
}
|
|
|
|
test('customer invoice failure retries only the invoice, even if the locked customer data is now invalid', async () => {
|
|
let fail = true
|
|
const h = customerHarness(
|
|
false,
|
|
{},
|
|
{
|
|
invoice: async () => {
|
|
if (fail) throw new Error('开票保存失败')
|
|
}
|
|
}
|
|
)
|
|
h.invoice.value = { companyName: '测试单位', bankAccount: '自由账号' }
|
|
await h.save()
|
|
assert.equal(h.state.customerId, 'created-customer')
|
|
assert.equal(h.error.value, '开票保存失败')
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.deepEqual(h.navigations, [])
|
|
h.form.value.shortName = ''
|
|
h.form.value.email = 'bad'
|
|
fail = false
|
|
await h.save()
|
|
assert.deepEqual(h.calls, ['confirm', 'ensure', 'duplicate', 'create', 'invoice', 'invoice'])
|
|
assert.equal(h.payloads.at(-1)?.customerId, 'created-customer')
|
|
assert.equal(h.payloads.at(-1)?.isDefault, '1')
|
|
assert.equal(h.navigations.length, 1)
|
|
})
|
|
|
|
test('customer duplicate detection preserves the draft and reports a visible reason', async () => {
|
|
const h = customerHarness(false, {}, { duplicate: async () => ({ isDuplicate: true }) })
|
|
const before = { ...h.form.value }
|
|
await h.save()
|
|
assert.deepEqual(h.calls, ['confirm', 'ensure', 'duplicate'])
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.match(h.error.value, /客户名称已存在/)
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.deepEqual(h.form.value, before)
|
|
assert.equal(h.saving.value, false)
|
|
})
|
|
|
|
test('a failed duplicate check performs no customer write and permits retry', async () => {
|
|
let fail = true
|
|
const h = customerHarness(
|
|
false,
|
|
{},
|
|
{
|
|
duplicate: async () => {
|
|
if (fail) throw new Error('查重失败,请重试')
|
|
return { isDuplicate: false }
|
|
}
|
|
}
|
|
)
|
|
const before = { ...h.form.value }
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.equal(h.error.value, '查重失败,请重试')
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.deepEqual(h.form.value, before)
|
|
assert.equal(h.saving.value, false)
|
|
fail = false
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 1)
|
|
assert.equal(h.navigations.length, 1)
|
|
})
|
|
|
|
test('optional invoice company name is still validated before both initial save and invoice retry', async () => {
|
|
for (const customerId of ['', 'created-customer']) {
|
|
const h = customerHarness()
|
|
h.state.customerId = customerId
|
|
h.invoice.value = { companyName: ' ', bankAccount: '已有账号' }
|
|
await h.save()
|
|
assert.deepEqual(h.calls, [])
|
|
assert.match(h.error.value, /单位名称不能为空/)
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.equal(h.state.customerId, customerId)
|
|
assert.equal(h.invoice.value.bankAccount, '已有账号')
|
|
}
|
|
})
|
|
|
|
test('customer attachment upload blocks all requests with visible feedback', async () => {
|
|
const h = customerHarness()
|
|
h.uploads.set('businessLicenseImages', { hasUploading: () => true })
|
|
await h.save()
|
|
assert.deepEqual(h.calls, [])
|
|
assert.equal(h.error.value, '附件上传中,请稍候')
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
})
|
|
|
|
function opportunityHarness(modeName: string, patch: model.Row = {}, write = async () => {}) {
|
|
const payloads: model.Row[] = []
|
|
const routes: string[] = []
|
|
const capture = (route: string) => async (data: model.Row) => {
|
|
routes.push(route)
|
|
payloads.push(data)
|
|
await write()
|
|
}
|
|
const bindings = {
|
|
...model,
|
|
...feedback(),
|
|
computed,
|
|
ref,
|
|
mode: ref(modeName),
|
|
busy: ref(false),
|
|
error: ref(''),
|
|
upload: ref(null),
|
|
customers: ref([]),
|
|
form: ref({
|
|
opportunityName: '测试商机',
|
|
customerId: 'customer-1',
|
|
stage: 'quote',
|
|
stagePercent: 60,
|
|
winRate: 65,
|
|
ownerUserId: 'owner-1',
|
|
changeReason: '推进原因',
|
|
followType: 'phone',
|
|
followTime: '2026-10-22T09:30',
|
|
followContent: '联系内容',
|
|
nextTime: '',
|
|
nextPlan: '下次计划',
|
|
attachments: 'fixture.pdf',
|
|
remark: '保留备注',
|
|
createTime: '只读字段',
|
|
status: 'following',
|
|
contactId: '不应覆盖关联联系人',
|
|
amount: 999,
|
|
...patch
|
|
}),
|
|
dictOptions: () => [],
|
|
OPPORTUNITY_TYPE_FALLBACK: {},
|
|
OPPORTUNITY_SOURCE_FALLBACK: {},
|
|
props: {
|
|
opportunity: { id: 'opportunity-1', stage: 'initial', contactId: 'contact-1', amount: 100 }
|
|
},
|
|
changeStage: capture('stage'),
|
|
updateOpportunity: capture('edit'),
|
|
addOpportunityFollow: capture('follow-create'),
|
|
updateOpportunityFollow: capture('follow-edit')
|
|
}
|
|
return {
|
|
...bindings,
|
|
...execute('OpportunityActions.vue', ['progress', 'fields', 'save'], bindings),
|
|
payloads,
|
|
routes
|
|
}
|
|
}
|
|
|
|
for (const mode of ['edit', 'stage']) {
|
|
for (const key of ['stagePercent', 'winRate']) {
|
|
for (const value of [-1, 101, NaN, Infinity, -Infinity, 'not-a-number']) {
|
|
test(`opportunity ${mode} rejects ${key}=${value} at the save entry`, async () => {
|
|
const h = opportunityHarness(mode, { [key]: value })
|
|
const before = { ...h.form.value }
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.match(h.error.value, key === 'winRate' ? /赢单率/ : /阶段进度/)
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.deepEqual(h.form.value, before)
|
|
assert.equal(h.mode.value, mode)
|
|
assert.equal(h.busy.value, false)
|
|
})
|
|
}
|
|
}
|
|
for (const value of [0, 7, 100, '0', '100']) {
|
|
test(`opportunity ${mode} accepts ${value} without a multiple-of-five restriction`, async () => {
|
|
const h = opportunityHarness(mode, { stagePercent: value, winRate: value })
|
|
await h.save()
|
|
assert.deepEqual(h.routes, [mode])
|
|
assert.equal(h.payloads[0].stagePercent, Number(value))
|
|
assert.equal(h.payloads[0].winRate, Number(value))
|
|
assert.equal(h.mode.value, '')
|
|
})
|
|
}
|
|
}
|
|
|
|
for (const [mode, input, expected] of [
|
|
['edit', 12.345, 12.35],
|
|
['edit', 1.005, 1.01],
|
|
['edit', 4.015, 4.02],
|
|
['edit', 99.995, 100],
|
|
['edit', 12.3449, 12.34],
|
|
['stage', 7.5, 8],
|
|
['stage', 7.4, 7]
|
|
] as const) {
|
|
test(`opportunity ${mode} follows Web input precision for ${input}`, async () => {
|
|
const h = opportunityHarness(mode, { stagePercent: input, winRate: input })
|
|
await h.save()
|
|
assert.equal(h.payloads[0].winRate, expected)
|
|
assert.equal(h.payloads[0].stagePercent, mode === 'edit' ? input : expected)
|
|
assert.equal(h.form.value.winRate, input)
|
|
})
|
|
}
|
|
|
|
test('opportunity edit retains hidden writable associations and excludes readonly fields', async () => {
|
|
const h = opportunityHarness('edit')
|
|
await h.save()
|
|
assert.equal(h.payloads[0].contactId, 'contact-1')
|
|
assert.equal(h.payloads[0].amount, 100)
|
|
assert.equal(h.payloads[0].id, 'opportunity-1')
|
|
assert.equal(h.payloads[0].remark, '保留备注')
|
|
assert.equal(Object.hasOwn(h.payloads[0], 'status'), false)
|
|
assert.equal(Object.hasOwn(h.payloads[0], 'createTime'), false)
|
|
})
|
|
|
|
test('opportunity stage rejects unchanged stage and preserves the draft', async () => {
|
|
const h = opportunityHarness('stage', { stage: 'initial' })
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.match(h.error.value, /相同/)
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.equal(h.mode.value, 'stage')
|
|
})
|
|
|
|
test('opportunity stage keeps its narrow payload', async () => {
|
|
const h = opportunityHarness('stage')
|
|
await h.save()
|
|
assert.deepEqual(h.payloads[0], {
|
|
id: 'opportunity-1',
|
|
stage: 'quote',
|
|
stagePercent: 60,
|
|
winRate: 65,
|
|
changeReason: '推进原因',
|
|
remark: '保留备注'
|
|
})
|
|
})
|
|
|
|
for (const mode of ['edit', 'stage', 'follow']) {
|
|
test(`opportunity ${mode} retains all required-field guards with field-specific feedback`, async () => {
|
|
const original = opportunityHarness(mode)
|
|
for (const field of original.fields.value.filter((f: model.Field) => f.required)) {
|
|
const h = opportunityHarness(mode, { [field.key]: ' ' })
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.ok(h.error.value.includes(field.label), h.error.value)
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
}
|
|
})
|
|
}
|
|
|
|
test('opportunity name minimum length remains enforced', async () => {
|
|
const h = opportunityHarness('edit', { opportunityName: '单' })
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 0)
|
|
assert.match(h.error.value, /至少两个/)
|
|
})
|
|
|
|
for (const edit of [false, true]) {
|
|
test(`opportunity follow ${edit ? 'edit' : 'create'} retains its payload and optional next date`, async () => {
|
|
const h = opportunityHarness('follow', edit ? { id: 'follow-1' } : {})
|
|
await h.save()
|
|
assert.deepEqual(h.routes, [edit ? 'follow-edit' : 'follow-create'])
|
|
assert.equal(h.payloads[0].opportunityId, 'opportunity-1')
|
|
assert.equal(h.payloads[0].followTime, '2026-10-22 09:30:00')
|
|
assert.equal(h.payloads[0].nextTime, '')
|
|
assert.equal(h.payloads[0].nextContent, '下次计划')
|
|
assert.equal(h.payloads[0].attachments, 'fixture.pdf')
|
|
})
|
|
}
|
|
|
|
const lifecycleFactories = {
|
|
contact: (write: () => Promise<void>) => {
|
|
const h = contactHarness(false, {}, write)
|
|
return { ...h, pending: h.acting, error: h.saveError, opened: () => h.editing.value }
|
|
},
|
|
customer: (write: () => Promise<void>) => {
|
|
const h = customerHarness(false, {}, { write })
|
|
return { ...h, pending: h.saving, opened: () => h.navigations.length === 0 }
|
|
},
|
|
opportunity: (write: () => Promise<void>) => {
|
|
const h = opportunityHarness('edit', {}, write)
|
|
return { ...h, pending: h.busy, opened: () => h.mode.value === 'edit' }
|
|
}
|
|
}
|
|
for (const [name, factory] of Object.entries(lifecycleFactories)) {
|
|
test(`${name} API failure retains draft and supports retry`, async () => {
|
|
let fail = true
|
|
const h = factory(async () => {
|
|
if (fail) throw new Error('保存失败测试')
|
|
})
|
|
const before = { ...h.form.value }
|
|
await h.save()
|
|
assert.equal(h.error.value, '保存失败测试')
|
|
assert.equal(h.notices.at(-1), h.error.value)
|
|
assert.equal(h.opened(), true)
|
|
assert.equal(h.pending.value, false)
|
|
assert.deepEqual(h.form.value, before)
|
|
fail = false
|
|
await h.save()
|
|
assert.equal(h.payloads.length, 2)
|
|
assert.equal(h.error.value, '')
|
|
})
|
|
test(`${name} pending save blocks a second submission`, async () => {
|
|
let resolve!: () => void
|
|
const h = factory(
|
|
() =>
|
|
new Promise<void>((done) => {
|
|
resolve = done
|
|
})
|
|
)
|
|
const pending = h.save()
|
|
await h.save()
|
|
while (!resolve) await Promise.resolve()
|
|
assert.equal(h.pending.value, true)
|
|
assert.equal(h.payloads.length, 1)
|
|
resolve()
|
|
await pending
|
|
assert.equal(h.pending.value, false)
|
|
})
|
|
}
|
|
|
|
test('number field bounds reach Vant and all affected SFCs compile', () => {
|
|
const source = readFileSync(new URL('CustomerFields.vue', base), 'utf8')
|
|
assert.match(source, /:min="field.minValue"/)
|
|
assert.match(source, /:max="field.maxValue"/)
|
|
for (const file of [
|
|
'CustomerFields.vue',
|
|
'ContactPanel.vue',
|
|
'form.vue',
|
|
'FollowPanel.vue',
|
|
'OpportunityActions.vue'
|
|
]) {
|
|
const { descriptor, errors } = parse(readFileSync(new URL(file, base), 'utf8'))
|
|
assert.deepEqual(errors, [])
|
|
const script = compileScript(descriptor, { id: file })
|
|
const template = compileTemplate({
|
|
id: file,
|
|
filename: file,
|
|
source: descriptor.template!.content,
|
|
compilerOptions: { bindingMetadata: script.bindings }
|
|
})
|
|
assert.deepEqual(template.errors, [])
|
|
}
|
|
})
|