703 lines
23 KiB
TypeScript
703 lines
23 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { parse } from '@vue/compiler-sfc'
|
|
import ts from 'typescript'
|
|
import * as vue from 'vue'
|
|
import { useClientColumnFilters } from '../src/composables/useClientColumnFilters'
|
|
import * as customerFilterDefinitions from '../src/views/crm/customer/dialogColumnFilterDefs'
|
|
import { contactFields } from '../src/views/m/crm/customer/model'
|
|
import { contactDetailsError, requiredFieldsError } from '../src/views/m/crm/customer/validation'
|
|
function deferred() {
|
|
let resolve!: (value?: any) => void
|
|
let reject!: (error: any) => void
|
|
const promise = new Promise<any>((a, b) => {
|
|
resolve = a
|
|
reject = b
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
const flush = async () => {
|
|
for (let i = 0; i < 20; i++) await Promise.resolve()
|
|
}
|
|
// Execute actual script-setup functions inside Vue's renderer; API responses alone are deferred.
|
|
const helperPath = new URL('../src/utils/mp-customer-panel-identity.ts', import.meta.url)
|
|
function evaluate(page: string, props: any, overrides: Record<string, any>, events: any[]) {
|
|
const source = parse(
|
|
readFileSync(new URL(`../src/views/m/crm/customer/${page}.vue`, import.meta.url), 'utf8')
|
|
).descriptor.scriptSetup!.content
|
|
const ast = ts.createSourceFile('panel.ts', source, ts.ScriptTarget.Latest, true)
|
|
const vueEntries = Object.entries(vue).filter(
|
|
([k]) => /^[a-zA-Z_$][\w$]*$/.test(k) && k !== 'default'
|
|
)
|
|
const bindings: Record<string, any> = Object.fromEntries(vueEntries)
|
|
delete bindings.default
|
|
for (const statement of ast.statements) {
|
|
if (!ts.isImportDeclaration(statement)) continue
|
|
const clause = statement.importClause
|
|
if (clause?.name) bindings[clause.name.text] = () => undefined
|
|
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings))
|
|
for (const item of clause.namedBindings.elements) bindings[item.name.text] = () => undefined
|
|
}
|
|
if (existsSync(helperPath)) {
|
|
const helper = ts.transpile(
|
|
readFileSync(helperPath, 'utf8').replace('export function', 'function'),
|
|
{ target: ts.ScriptTarget.ESNext }
|
|
)
|
|
bindings.useCustomerPanelIdentity = new Function(
|
|
...vueEntries.map(([k]) => k),
|
|
`${helper};return useCustomerPanelIdentity`
|
|
)(...vueEntries.map(([, v]) => v))
|
|
}
|
|
Object.assign(bindings, {
|
|
useClientColumnFilters,
|
|
...customerFilterDefinitions,
|
|
defineProps: () => props,
|
|
defineEmits:
|
|
() =>
|
|
(...args: any[]) =>
|
|
events.push(args),
|
|
defineExpose() {},
|
|
useRouter: () => ({ push: async () => {} }),
|
|
useDictStore: () => ({ getOptions: () => [], getLabel: () => '', getTagType: () => '' }),
|
|
unwrapRows: (v: any) => (Array.isArray(v) ? v : v?.data?.rows || v?.data || v?.rows || []),
|
|
unwrapData: (v: any) => v?.data || v,
|
|
canDeleteByCreator: () => true,
|
|
contactFields: [{ key: 'contactName', required: true }],
|
|
invoiceFields: [{ key: 'companyName' }],
|
|
requiredFieldsError: () => '',
|
|
contactDetailsError: () => '',
|
|
percentageError: () => '',
|
|
apiTime: (v: any) => v,
|
|
opportunityProgress: () => ({}),
|
|
showCrmConfirmDialog: async () => {},
|
|
fetchContactByCustomerId: async () => [],
|
|
fetchFollowByCustomerId: async () => [],
|
|
getInvoiceInfoByCustomerId: async () => [],
|
|
fetchContactDetail: async (v: any) => v,
|
|
fetchUserOptions: async () => [],
|
|
fetchCustomerOptions: async () => [],
|
|
listOpportunity: async () => [],
|
|
listContract: async () => [],
|
|
listOperLog: async () => [],
|
|
getOpportunity: async (id: string) => ({ id }),
|
|
listFollowByOpportunity: async () => [],
|
|
getStageLogList: async () => [],
|
|
...overrides
|
|
})
|
|
const statements = ast.statements.filter((s) => !ts.isImportDeclaration(s))
|
|
const names = statements.flatMap((s) =>
|
|
ts.isFunctionDeclaration(s)
|
|
? [s.name!.text]
|
|
: ts.isVariableStatement(s)
|
|
? s.declarationList.declarations.map((d) => d.name.getText(ast))
|
|
: []
|
|
)
|
|
const code = ts.transpile(statements.map((s) => s.getText(ast)).join('\n'), {
|
|
target: ts.ScriptTarget.ESNext
|
|
})
|
|
return new Function(...Object.keys(bindings), `${code};return {${names.join(',')}}`)(
|
|
...Object.values(bindings)
|
|
)
|
|
}
|
|
function mount(page: string, overrides: Record<string, any> = {}) {
|
|
const routeState = vue.reactive({ params: { id: 'A' }, query: { tab: 'basic' }, path: '/A' })
|
|
const route = {
|
|
get id() {
|
|
return routeState.params.id
|
|
},
|
|
set id(id: string) {
|
|
routeState.params.id = id
|
|
},
|
|
get tab() {
|
|
return routeState.query.tab
|
|
},
|
|
set tab(tab: string) {
|
|
routeState.query.tab = tab
|
|
}
|
|
}
|
|
const events: any[] = []
|
|
let api: any
|
|
let actions: any
|
|
const renderer = vue.createRenderer<any, any>({
|
|
patchProp() {},
|
|
insert() {},
|
|
remove() {},
|
|
createElement: () => ({}),
|
|
createText: () => ({}),
|
|
createComment: () => ({}),
|
|
setText() {},
|
|
setElementText() {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null
|
|
})
|
|
const NestedActions = vue.defineComponent({
|
|
props: ['opportunity', 'getIdentity'],
|
|
setup(props) {
|
|
actions = evaluate('OpportunityActions', props, overrides, events)
|
|
return () => vue.h('div')
|
|
}
|
|
})
|
|
const Child = vue.defineComponent({
|
|
props: ['customerId', 'opportunity', 'getIdentity', 'kind'],
|
|
setup(props) {
|
|
api = evaluate(
|
|
page === 'RelatedPanelActions' ? 'RelatedPanel' : page,
|
|
props,
|
|
overrides,
|
|
events
|
|
)
|
|
return () =>
|
|
page === 'RelatedPanelActions' && api.detail.value && api.detailOpen.value
|
|
? vue.h(NestedActions, { opportunity: api.detail.value, getIdentity: api.identity.key })
|
|
: vue.h('div')
|
|
}
|
|
})
|
|
const Parent = vue.defineComponent({
|
|
setup() {
|
|
const parent = evaluate(
|
|
'detail',
|
|
{},
|
|
{ useRoute: () => routeState, useRouter: () => ({ replace() {}, push() {} }) },
|
|
[]
|
|
)
|
|
const getIdentity = parent.getPanelIdentity || (() => route.id)
|
|
return () =>
|
|
vue.h(Child, {
|
|
customerId: route.id,
|
|
opportunity: { id: route.id, status: 'following' },
|
|
kind: 'opportunity',
|
|
getIdentity
|
|
})
|
|
}
|
|
})
|
|
const app = renderer.createApp(Parent)
|
|
app.mount({})
|
|
return {
|
|
get api() {
|
|
return api
|
|
},
|
|
get actions() {
|
|
return actions
|
|
},
|
|
route,
|
|
events,
|
|
unmount: () => app.unmount()
|
|
}
|
|
}
|
|
const listApis: Record<string, string> = {
|
|
ContactPanel: 'fetchContactByCustomerId',
|
|
FollowPanel: 'fetchFollowByCustomerId',
|
|
InvoicePanel: 'getInvoiceInfoByCustomerId',
|
|
RelatedPanel: 'listOpportunity'
|
|
}
|
|
test('ContactPanel: editing visible fields preserves hidden contact values in the update payload', async () => {
|
|
const writes: any[] = []
|
|
const original = {
|
|
id: 'contact-A',
|
|
contactName: '验收联系人',
|
|
mobile: '19900000000',
|
|
qq: 'synthetic-qq',
|
|
birthday: '2000-02-29',
|
|
address: '验收合成住址',
|
|
department: '原部门'
|
|
}
|
|
const h = mount('ContactPanel', {
|
|
contactFields,
|
|
contactDetailsError,
|
|
requiredFieldsError,
|
|
fetchContactDetail: async () => ({ data: original }),
|
|
fetchUpdateContact: async (payload: any) => writes.push(payload)
|
|
})
|
|
try {
|
|
await h.api.open({ id: original.id })
|
|
assert.equal(h.api.visibleContactFields.value.some((field: any) => field.hidden), false)
|
|
h.api.form.value.department = '已改部门'
|
|
await h.api.save()
|
|
assert.equal(writes.length, 1)
|
|
assert.equal(writes[0].department, '已改部门')
|
|
for (const key of ['qq', 'birthday', 'address']) assert.equal(writes[0][key], original[key])
|
|
assert.equal(writes[0].id, original.id)
|
|
assert.equal(writes[0].customerId, 'A')
|
|
assert.equal(original.department, '原部门')
|
|
assert.equal(h.api.editing.value, false)
|
|
} finally {
|
|
h.unmount()
|
|
}
|
|
})
|
|
|
|
test('ContactPanel: opening another contact does not carry hidden fields from the previous draft', async () => {
|
|
const writes: any[] = []
|
|
const h = mount('ContactPanel', {
|
|
contactFields,
|
|
contactDetailsError,
|
|
requiredFieldsError,
|
|
fetchContactDetail: async ({ id }: { id: string }) => ({
|
|
data: {
|
|
id,
|
|
contactName: id,
|
|
mobile: '19900000000',
|
|
...(id === 'contact-A'
|
|
? { qq: 'synthetic-qq', birthday: '2000-02-29', address: '验收合成住址' }
|
|
: { qq: null })
|
|
}
|
|
}),
|
|
fetchUpdateContact: async (payload: any) => writes.push(payload)
|
|
})
|
|
try {
|
|
await h.api.open({ id: 'contact-A' })
|
|
h.api.closeEditing()
|
|
await h.api.open({ id: 'contact-B' })
|
|
h.api.form.value.department = '第二位联系人部门'
|
|
await h.api.save()
|
|
assert.equal(writes.length, 1)
|
|
assert.equal(writes[0].id, 'contact-B')
|
|
for (const key of ['qq', 'birthday', 'address']) assert.equal(writes[0][key], '')
|
|
} finally {
|
|
h.unmount()
|
|
}
|
|
})
|
|
|
|
test('ContactPanel: failed update retains hidden values and retries the same edited payload', async () => {
|
|
const writes: any[] = []
|
|
const h = mount('ContactPanel', {
|
|
contactFields,
|
|
contactDetailsError,
|
|
requiredFieldsError,
|
|
fetchContactDetail: async () => ({
|
|
data: {
|
|
id: 'contact-A',
|
|
contactName: '验收联系人',
|
|
mobile: '19900000000',
|
|
qq: 'synthetic-qq',
|
|
birthday: '2000-02-29',
|
|
address: '验收合成住址'
|
|
}
|
|
}),
|
|
fetchUpdateContact: async (payload: any) => {
|
|
writes.push(payload)
|
|
if (writes.length === 1) throw new Error('验收模拟保存失败')
|
|
}
|
|
})
|
|
try {
|
|
await h.api.open({ id: 'contact-A' })
|
|
h.api.form.value.department = '重试仍保留的新部门'
|
|
await h.api.save()
|
|
assert.equal(h.api.editing.value, true)
|
|
assert.equal(h.api.acting.value, false)
|
|
assert.equal(h.api.saveError.value, '验收模拟保存失败')
|
|
assert.equal(h.api.form.value.birthday, '2000-02-29')
|
|
assert.equal(h.api.form.value.department, '重试仍保留的新部门')
|
|
await h.api.save()
|
|
assert.equal(writes.length, 2)
|
|
assert.deepEqual(writes[1], writes[0])
|
|
assert.equal(writes[1].qq, 'synthetic-qq')
|
|
assert.equal(writes[1].address, '验收合成住址')
|
|
assert.equal(h.api.editing.value, false)
|
|
assert.equal(h.api.saveError.value, '')
|
|
} finally {
|
|
h.unmount()
|
|
}
|
|
})
|
|
|
|
test('InvoicePanel: setting default announces success only after the write succeeds', async () => {
|
|
const write = deferred()
|
|
const messages: string[] = []
|
|
const h = mount('InvoicePanel', {
|
|
request: { post: () => write.promise },
|
|
showSuccessToast: (message: string) => messages.push(message)
|
|
})
|
|
try {
|
|
await flush()
|
|
const pending = h.api.setDefault({ id: 'invoice-A' })
|
|
assert.deepEqual(messages, [])
|
|
write.resolve()
|
|
await pending
|
|
assert.deepEqual(messages, ['设置成功'])
|
|
assert.equal(h.api.acting.value, false)
|
|
} finally {
|
|
h.unmount()
|
|
}
|
|
})
|
|
test('InvoicePanel: rejected default write retains its error without announcing success', async () => {
|
|
const messages: string[] = []
|
|
const h = mount('InvoicePanel', {
|
|
request: { post: async () => { throw new Error('验收请求失败') } },
|
|
showSuccessToast: (message: string) => messages.push(message)
|
|
})
|
|
try {
|
|
await flush()
|
|
await h.api.setDefault({ id: 'invoice-A' })
|
|
assert.deepEqual(messages, [])
|
|
assert.equal(h.api.error.value, '验收请求失败')
|
|
assert.equal(h.api.acting.value, false)
|
|
} finally {
|
|
h.unmount()
|
|
}
|
|
})
|
|
for (const outcome of ['switch', 'ABA', 'unmount', 'same-customer-tab']) {
|
|
test(`InvoicePanel: default success feedback respects ${outcome}`, async () => {
|
|
const write = deferred()
|
|
const messages: string[] = []
|
|
const h = mount('InvoicePanel', {
|
|
request: { post: () => write.promise },
|
|
showSuccessToast: (message: string) => messages.push(message)
|
|
})
|
|
await flush()
|
|
const pending = h.api.setDefault({ id: 'invoice-A' })
|
|
if (outcome === 'unmount') h.unmount()
|
|
else if (outcome === 'same-customer-tab') h.route.tab = 'invoice'
|
|
else {
|
|
h.route.id = 'B'
|
|
if (outcome === 'ABA') h.route.id = 'A'
|
|
}
|
|
write.resolve()
|
|
try {
|
|
await pending
|
|
assert.deepEqual(messages, outcome === 'same-customer-tab' ? ['设置成功'] : [])
|
|
} finally {
|
|
if (outcome !== 'unmount') h.unmount()
|
|
}
|
|
})
|
|
}
|
|
for (const [page, read] of Object.entries(listApis)) {
|
|
test(`${page}: late initial load cannot overwrite B or ABA`, async () => {
|
|
const old = deferred()
|
|
let calls = 0
|
|
const h = mount(page, {
|
|
[read]: () => (++calls === 1 ? old.promise : Promise.resolve([{ id: 'fresh' }]))
|
|
})
|
|
h.route.id = 'B'
|
|
await flush()
|
|
h.route.id = 'A'
|
|
await flush()
|
|
old.resolve([{ id: 'old' }])
|
|
await flush()
|
|
assert.deepEqual(h.api.rows.value, [{ id: 'fresh' }])
|
|
h.unmount()
|
|
})
|
|
}
|
|
const deletes: Record<string, string> = {
|
|
ContactPanel: 'fetchDeleteContact',
|
|
FollowPanel: 'fetchDeleteCustomerFollow',
|
|
InvoicePanel: 'delInvoiceInfo',
|
|
OpportunityActions: 'delOpportunity'
|
|
}
|
|
for (const [page, write] of Object.entries(deletes)) {
|
|
for (const outcome of ['switch', 'ABA', 'unmount'])
|
|
test(`${page}: confirmation resolves before same-tick ${outcome}`, async () => {
|
|
const confirm = deferred(),
|
|
writes: any[] = []
|
|
const h = mount(page, {
|
|
showCrmConfirmDialog: () => confirm.promise,
|
|
[write]: async (v: any) => writes.push(v)
|
|
})
|
|
await flush()
|
|
const pending = h.api.remove(page === 'OpportunityActions' ? undefined : { id: 'row-A' })
|
|
confirm.resolve()
|
|
if (outcome === 'unmount') h.unmount()
|
|
else {
|
|
h.route.id = 'B'
|
|
if (outcome === 'ABA') h.route.id = 'A'
|
|
}
|
|
await pending
|
|
assert.deepEqual(writes, [])
|
|
assert.deepEqual(h.events, [])
|
|
if (outcome !== 'unmount') h.unmount()
|
|
})
|
|
test(`${page}: same-ID tab query retains legitimate confirmation`, async () => {
|
|
const confirm = deferred(),
|
|
writes: any[] = []
|
|
const h = mount(page, {
|
|
showCrmConfirmDialog: () => confirm.promise,
|
|
[write]: async (v: any) => writes.push(v)
|
|
})
|
|
await flush()
|
|
const pending = h.api.remove(page === 'OpportunityActions' ? undefined : { id: 'row-A' })
|
|
h.route.tab = 'contact'
|
|
confirm.resolve()
|
|
await pending
|
|
assert.equal(writes.length, 1)
|
|
h.unmount()
|
|
})
|
|
}
|
|
test('ContactPanel: reversed detail opens retain latest row', async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
const h = mount('ContactPanel', {
|
|
fetchContactDetail: ({ id }: any) => (id === 'one' ? first.promise : second.promise)
|
|
})
|
|
const a = h.api.open({ id: 'one' }),
|
|
b = h.api.open({ id: 'two' })
|
|
second.resolve({ id: 'two' })
|
|
await b
|
|
first.resolve({ id: 'one' })
|
|
await a
|
|
assert.equal(h.api.form.value.id, 'two')
|
|
h.unmount()
|
|
})
|
|
test('OpportunityActions: reversed customer searches retain latest query', async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
const h = mount('OpportunityActions', {
|
|
fetchCustomerOptions: ({ keyword }: any) => (keyword === 'one' ? first.promise : second.promise)
|
|
})
|
|
await h.api.open('edit')
|
|
h.api.customerKeyword.value = 'one'
|
|
const a = h.api.searchCustomers()
|
|
h.api.customerKeyword.value = 'two'
|
|
const b = h.api.searchCustomers()
|
|
second.resolve([{ id: 'two', customerName: 'two' }])
|
|
await b
|
|
first.resolve([{ id: 'one', customerName: 'one' }])
|
|
await a
|
|
assert.equal(h.api.customers.value[0].value, 'two')
|
|
h.unmount()
|
|
})
|
|
const saves: Record<string, string> = {
|
|
ContactPanel: 'fetchCreateContact',
|
|
FollowPanel: 'fetchCreateCustomerFollow',
|
|
InvoicePanel: 'addInvoiceInfo',
|
|
OpportunityActions: 'addOpportunityFollow'
|
|
}
|
|
for (const [page, write] of Object.entries(saves)) {
|
|
test(`${page}: old save completion does not close, emit or clear new busy`, async () => {
|
|
const old = deferred(),
|
|
fresh = deferred()
|
|
let count = 0
|
|
const h = mount(page, { [write]: () => (++count === 1 ? old.promise : fresh.promise) })
|
|
await flush()
|
|
await (page === 'OpportunityActions' ? h.api.open('follow') : h.api.open())
|
|
Object.assign(h.api.form.value, {
|
|
contactName: 'A',
|
|
companyName: 'A',
|
|
followContent: 'A',
|
|
followType: 'phone'
|
|
})
|
|
const a = h.api.save()
|
|
h.route.id = 'B'
|
|
await flush()
|
|
await (page === 'OpportunityActions' ? h.api.open('follow') : h.api.open())
|
|
Object.assign(h.api.form.value, {
|
|
contactName: 'B',
|
|
companyName: 'B',
|
|
followContent: 'B',
|
|
followType: 'phone'
|
|
})
|
|
const b = h.api.save()
|
|
old.resolve()
|
|
await a
|
|
assert.equal((h.api.acting || h.api.busy).value, true)
|
|
assert.equal(
|
|
(h.api.editing || h.api.mode).value,
|
|
page === 'OpportunityActions' ? 'follow' : true
|
|
)
|
|
assert.deepEqual(h.events, [])
|
|
fresh.resolve()
|
|
await b
|
|
assert.equal((h.api.acting || h.api.busy).value, false)
|
|
h.unmount()
|
|
})
|
|
}
|
|
test('FollowPanel: close/reopen ignores old contacts error', async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
let calls = 0
|
|
const h = mount('FollowPanel', {
|
|
fetchContactByCustomerId: () => (++calls === 1 ? first.promise : second.promise)
|
|
})
|
|
const a = h.api.open()
|
|
h.api.closeEditing()
|
|
const b = h.api.open()
|
|
second.resolve([{ id: 'new' }])
|
|
await b
|
|
first.reject(new Error('old error'))
|
|
await a
|
|
assert.equal(h.api.saveError.value, '')
|
|
assert.deepEqual(h.api.contacts.value, [{ id: 'new' }])
|
|
h.unmount()
|
|
})
|
|
test('RelatedPanel: reversed opportunity opens keep fresh detail and actions', async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
const h = mount('RelatedPanel', {
|
|
getOpportunity: (id: string) => (id === 'one' ? first.promise : second.promise)
|
|
})
|
|
const a = h.api.open({ id: 'one' }),
|
|
b = h.api.open({ id: 'two' })
|
|
second.resolve({ id: 'two' })
|
|
await b
|
|
first.resolve({ id: 'one' })
|
|
await a
|
|
assert.equal(h.api.detail.value.id, 'two')
|
|
h.unmount()
|
|
})
|
|
for (const transition of ['openB', 'close', 'closeReopenA', 'routeABA']) {
|
|
test(`nested RelatedPanel/OpportunityActions: confirmation cannot survive ${transition}`, async () => {
|
|
const confirm = deferred(),
|
|
writes: any[] = []
|
|
const h = mount('RelatedPanelActions', {
|
|
showCrmConfirmDialog: () => confirm.promise,
|
|
delOpportunity: async (id: string) => writes.push(id)
|
|
})
|
|
await flush()
|
|
await h.api.open({ id: 'opportunity-A' })
|
|
await flush()
|
|
const pending = h.actions.remove()
|
|
confirm.resolve()
|
|
if (transition === 'openB') void h.api.open({ id: 'opportunity-B' })
|
|
if (transition === 'close') h.api.closeDetail()
|
|
if (transition === 'closeReopenA') {
|
|
h.api.closeDetail()
|
|
void h.api.open({ id: 'opportunity-A' })
|
|
}
|
|
if (transition === 'routeABA') {
|
|
h.route.id = 'B'
|
|
h.route.id = 'A'
|
|
}
|
|
await pending
|
|
assert.deepEqual(writes, [])
|
|
assert.deepEqual(h.events, [])
|
|
h.unmount()
|
|
})
|
|
}
|
|
for (const [page, read] of Object.entries(listApis)) {
|
|
test(`${page}: old list error and finally cannot overwrite B loading`, async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
let calls = 0
|
|
const h = mount(page, { [read]: () => (++calls === 1 ? first.promise : second.promise) })
|
|
h.route.id = 'B'
|
|
await flush()
|
|
first.reject(new Error('old error'))
|
|
await flush()
|
|
assert.equal(h.api.error.value, '')
|
|
assert.equal(h.api.loading.value, true)
|
|
second.resolve([{ id: 'B' }])
|
|
await flush()
|
|
assert.deepEqual(h.api.rows.value, [{ id: 'B' }])
|
|
h.unmount()
|
|
})
|
|
}
|
|
test('ContactPanel: primary confirmation identifies the selected contact before writing', async () => {
|
|
const confirmation = deferred()
|
|
const prompts: any[] = []
|
|
const writes: any[] = []
|
|
const h = mount('ContactPanel', {
|
|
showCrmConfirmDialog: (prompt: any) => {
|
|
prompts.push(prompt)
|
|
return confirmation.promise
|
|
},
|
|
fetchSetPrimaryContact: async (value: any) => writes.push(value)
|
|
})
|
|
try {
|
|
await flush()
|
|
const pending = h.api.primary({ id: 'contact-A', contactName: '验收联系人甲' })
|
|
assert.equal(prompts[0].message, '确认将「验收联系人甲」设置为主要联系人吗?')
|
|
assert.deepEqual(writes, [])
|
|
confirmation.reject('cancel')
|
|
await pending
|
|
assert.deepEqual(writes, [])
|
|
assert.equal(h.api.error.value, '')
|
|
} finally {
|
|
confirmation.reject('cancel')
|
|
h.unmount()
|
|
}
|
|
})
|
|
for (const outcome of ['success', 'failure', 'switch', 'ABA', 'unmount']) {
|
|
test(`ContactPanel: primary feedback respects ${outcome}`, async () => {
|
|
const write = deferred()
|
|
const messages: string[] = []
|
|
const h = mount('ContactPanel', {
|
|
fetchSetPrimaryContact: () => write.promise,
|
|
showSuccessToast: (message: string) => messages.push(message)
|
|
})
|
|
await flush()
|
|
const pending = h.api.primary({ id: 'contact-A', contactName: '验收联系人甲' })
|
|
await flush()
|
|
assert.deepEqual(messages, [])
|
|
if (outcome === 'unmount') h.unmount()
|
|
if (outcome === 'switch' || outcome === 'ABA') {
|
|
h.route.id = 'B'
|
|
if (outcome === 'ABA') h.route.id = 'A'
|
|
}
|
|
if (outcome === 'failure') write.reject(new Error('验收请求失败'))
|
|
else write.resolve()
|
|
try {
|
|
await pending
|
|
assert.deepEqual(messages, outcome === 'success' ? ['设置成功'] : [])
|
|
if (outcome === 'failure') assert.equal(h.api.error.value, '验收请求失败')
|
|
} finally {
|
|
if (outcome !== 'unmount') h.unmount()
|
|
}
|
|
})
|
|
}
|
|
test('ContactPanel: stale primary confirmation cannot set a row on the next customer', async () => {
|
|
const confirm = deferred(),
|
|
writes: any[] = []
|
|
const h = mount('ContactPanel', {
|
|
showCrmConfirmDialog: () => confirm.promise,
|
|
fetchSetPrimaryContact: async (v: any) => writes.push(v)
|
|
})
|
|
await flush()
|
|
const pending = h.api.primary({ id: 'contact-A' })
|
|
confirm.resolve()
|
|
h.route.id = 'B'
|
|
await pending
|
|
assert.deepEqual(writes, [])
|
|
h.unmount()
|
|
})
|
|
test('InvoicePanel: default write failure cannot replace the next customer error or busy', async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
let calls = 0
|
|
const h = mount('InvoicePanel', {
|
|
request: { post: () => (++calls === 1 ? first.promise : second.promise) }
|
|
})
|
|
await flush()
|
|
const a = h.api.setDefault({ id: 'invoice-A' })
|
|
h.route.id = 'B'
|
|
await flush()
|
|
const b = h.api.setDefault({ id: 'invoice-B' })
|
|
first.reject(new Error('old error'))
|
|
await a
|
|
assert.equal(h.api.error.value, '')
|
|
assert.equal(h.api.acting.value, true)
|
|
second.resolve()
|
|
await b
|
|
assert.equal(h.api.acting.value, false)
|
|
h.unmount()
|
|
})
|
|
test('OpportunityActions: old owner options cannot replace a reopened edit session', async () => {
|
|
const first = deferred(),
|
|
second = deferred()
|
|
let calls = 0
|
|
const h = mount('OpportunityActions', {
|
|
fetchUserOptions: () => (++calls === 1 ? first.promise : second.promise)
|
|
})
|
|
const a = h.api.open('edit')
|
|
const b = h.api.open('edit')
|
|
second.resolve([{ value: 'B', label: 'B' }])
|
|
await b
|
|
first.resolve([{ value: 'A', label: 'A' }])
|
|
await a
|
|
assert.deepEqual(h.api.users.value, [{ value: 'B', label: 'B' }])
|
|
h.unmount()
|
|
})
|
|
test('OpportunityActions: delete uses the captured follow ID', async () => {
|
|
const confirm = deferred(),
|
|
writes: any[] = []
|
|
const h = mount('OpportunityActions', {
|
|
showCrmConfirmDialog: () => confirm.promise,
|
|
delOpportunityFollow: async (v: any) => writes.push(v)
|
|
})
|
|
const row = { id: 'follow-A' }
|
|
const pending = h.api.remove(row)
|
|
row.id = 'follow-B'
|
|
confirm.resolve()
|
|
await pending
|
|
assert.deepEqual(writes, ['follow-A'])
|
|
assert.deepEqual(h.events, [['changed']])
|
|
h.unmount()
|
|
})
|