Files
shanxia2.0/web-admin/scripts/mobile-customer-sublist-parity.test.ts
T

678 lines
26 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 defs from '../src/views/crm/customer/dialogColumnFilterDefs'
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,
...defs,
OPPORTUNITY_SOURCE_FALLBACK: {},
OPPORTUNITY_TYPE_FALLBACK: {},
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.flatMap((d) =>
ts.isObjectBindingPattern(d.name)
? d.name.elements.map((e) => e.name.getText(ast))
: [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, kind = 'contract', extraProps: any = {}, overrides: any = {}) {
let api: any
const props = vue.shallowReactive({ customerId: 'A', kind, ...extraProps })
const renderer = vue.createRenderer<any, any>({
patchProp() {},
insert() {},
remove() {},
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText() {},
setElementText() {},
parentNode: () => null,
nextSibling: () => null
})
const app = renderer.createApp({
setup() {
api = evaluate(page, props, overrides, [])
return () => vue.h('div')
}
})
app.mount({})
return { api, props, stop: () => app.unmount() }
}
test('联系人姓名手机号组合、枚举并集和快捷筛选共享状态', async () => {
const { api, stop } = mount('ContactPanel')
await flush()
assert.ok(api.filterApi, '面板应使用 Web 客户端筛选 API')
api.rows.value = [
{ id: '1', contactName: '张甲', mobile: '138', gender: 'male', role: 'a' },
{ id: '2', contactName: '张乙', mobile: '139', gender: 'female', role: 'b' },
{ id: '3', contactName: '李甲', mobile: '138', gender: 'male', role: 'c' }
]
api.filterApi.getFilterState('contactName').text = '张'
api.filterApi.getFilterState('mobile').text = '138'
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['1']
)
api.resetMore()
api.filterApi.getFilterState('role').selected = ['a', 'b']
assert.equal(api.filtered.value.length, 2)
api.roleFilter.value = 'c'
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['3']
)
api.resetMore()
assert.equal(api.roleFilter.value, '')
assert.equal(api.filtered.value.length, 3)
stop()
})
test('联系人快速搜索同时匹配角色显示名称和原始编码', async () => {
const { api, stop } = mount('ContactPanel', 'contract', {}, {
useDictStore: () => ({
getOptions: () => [],
getLabel: (_code: string, value: string) =>
({ decision: '决策人', manager: '管理者' })[value] || value || ''
})
})
try {
await flush()
api.rows.value = [
{ id: '1', contactName: '张甲', role: 'decision' },
{ id: '2', contactName: '李乙', role: 'manager' },
{ id: '3', contactName: '王丙', role: 'legacy' }
]
for (const [keyword, expected] of [
['决策人', ['1']],
[' 策人 ', ['1']],
['DECISION', ['1']],
['管理', ['2']],
['legacy', ['3']],
['不存在角色', []],
['', ['1', '2', '3']]
] as Array<[string, string[]]>) {
api.keyword.value = keyword
assert.deepEqual(api.filtered.value.map((row: any) => row.id), expected, keyword)
}
} finally {
stop()
}
})
test('联系人角色名称搜索与列筛选取交集,清空不会丢失原筛选', async () => {
const { api, stop } = mount('ContactPanel', 'contract', {}, {
useDictStore: () => ({
getOptions: () => [],
getLabel: (_code: string, value: string) => value === 'decision' ? '决策人' : value
})
})
try {
await flush()
api.rows.value = [
{ id: '1', role: 'decision', gender: 'male' },
{ id: '2', role: 'decision', gender: 'female' },
{ id: '3', role: 'manager', gender: 'female' }
]
api.genderFilter.value = 'female'
api.keyword.value = '决策人'
assert.deepEqual(api.filtered.value.map((row: any) => row.id), ['2'])
api.roleFilter.value = 'manager'
assert.deepEqual(api.filtered.value, [])
api.keyword.value = ''
assert.deepEqual(api.filtered.value.map((row: any) => row.id), ['3'])
} finally {
stop()
}
})
test('联系人角色字典后加载时重新计算名称搜索结果', async () => {
const labels = vue.reactive<Record<string, string>>({})
const { api, stop } = mount('ContactPanel', 'contract', {}, {
useDictStore: () => ({
getOptions: () => [],
getLabel: (_code: string, value: string) => labels[value] || value || ''
})
})
try {
await flush()
api.rows.value = [{ id: '1', role: 'decision' }]
api.keyword.value = '决策人'
assert.deepEqual(api.filtered.value, [])
labels.decision = '决策人'
assert.deepEqual(api.filtered.value.map((row: any) => row.id), ['1'])
labels.decision = '决策负责人'
assert.deepEqual(api.filtered.value, [])
api.keyword.value = '决策负责人'
assert.deepEqual(api.filtered.value.map((row: any) => row.id), ['1'])
} finally {
stop()
}
})
test('开票默认多选与单位税号组合即时筛选', async () => {
const { api, stop } = mount('InvoicePanel')
await flush()
assert.ok(api.filterApi)
api.rows.value = [
{ id: '1', companyName: '甲', taxNo: '123', isDefault: 1 },
{ id: '2', companyName: '甲', taxNo: '234', isDefault: 0 },
{ id: '3', companyName: '乙', taxNo: '', isDefault: 0 }
]
api.filterApi.getFilterState('isDefault').selected = ['1', '0']
assert.equal(api.filtered.value.length, 3)
api.filterApi.getFilterState('companyName').text = '甲'
api.filterApi.getFilterState('taxNo').text = '123'
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['1']
)
api.resetMore()
assert.equal(api.filtered.value.length, 3)
stop()
})
for (const [kind, date, number] of [
['contract', 'signDate', 'amount'],
['log', 'operTime', 'costTime'],
['opportunity', 'expectDealDate', '']
])
test(`${kind} 日期范围及数值比较与 Web 一致`, async () => {
const { api, props, stop } = mount('RelatedPanel', kind)
await flush()
assert.ok(api.filterApi)
api.rows.value = [10, 20, 30].map((value, i) => ({
id: String(i),
[date]: `2026-10-${21 + i} 23:59:59`,
[number]: value,
stage: String(i),
status: String(i),
contractType: String(i),
businessType: String(i)
}))
Object.assign(api.filterApi.getFilterState(date), { from: '2026-10-22', to: '2026-10-22' })
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['1']
)
api.resetMore()
assert.equal(api.filtered.value.length, 3)
if (number) {
for (const [op, ids] of [
['eq', ['1']],
['gt', ['2']],
['gte', ['1', '2']],
['lt', ['0']],
['lte', ['0', '1']],
['between', ['1']]
]) {
Object.assign(api.filterApi.getFilterState(number), {
op,
value: '20',
min: '15',
max: '25'
})
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
ids
)
}
}
api.resetMore()
const prop = kind === 'contract' ? 'contractType' : kind === 'log' ? 'businessType' : 'stage'
api.filterApi.getFilterState(prop).selected = ['0', '2']
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['0', '2']
)
api.quick1.value = '1'
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['1']
)
api.resetMore()
api.rows.value = []
assert.equal(api.filtered.value.length, 0, '刷新集合为空时不保留子组件缓存行')
props.customerId = 'B'
await vue.nextTick()
assert.equal(api.filtered.value.length, 0)
if (kind === 'opportunity') {
assert.equal(api.listFields.value.filter((f: any) => f.key === 'createTime').length, 1)
assert.equal(api.detailFields.value.filter((f: any) => f.key === 'createTime').length, 1)
}
stop()
})
test('移动筛选控件使用父级状态、枚举搜索/清空和日期/比较器事件即时生效', () => {
const rows = vue.ref([
{ gender: 'male', amount: 10, signDate: '2026-10-22' },
{ gender: 'female', amount: 30, signDate: '2026-10-23' }
])
const definitions = [
...defs.buildDetailContactColumnFilterDefs(() => []),
...defs.buildDetailContractColumnFilterDefs(() => [])
]
const filterApi = useClientColumnFilters(rows, definitions)
const { api, stop } = mount('LocalFilters', '', { filterApi, definitions, rows: rows.value })
api.toggleEnum('gender', 'male')
assert.equal(filterApi.filteredRows.value.length, 1)
api.enumKeywords.gender = '女'
assert.deepEqual(
api.enumOptions(definitions.find((d: any) => d.prop === 'gender')).map((o: any) => o.value),
['female']
)
api.toggleEnum('gender', 'female')
assert.equal(filterApi.filteredRows.value.length, 2)
api.clearField('gender')
assert.equal(filterApi.hasActiveFilters.value, false)
api.openDate('signDate', 'from')
api.dateParts.value = ['2026', '10', '23']
api.onDateConfirm()
assert.equal(filterApi.filteredRows.value.length, 1)
api.clearField('signDate')
assert.equal(filterApi.filteredRows.value.length, 2)
api.openOp('amount')
api.onOpConfirm({ selectedValues: ['gte'] })
api.setRangeValue('amount', 'value', '30')
assert.equal(filterApi.filteredRows.value.length, 1)
api.clearAllFilters()
assert.equal(filterApi.filteredRows.value.length, 2)
stop()
})
for (const [page, kind, builder] of [
['ContactPanel', '', defs.buildDetailContactColumnFilterDefs],
['InvoicePanel', '', defs.buildInvoiceInfoColumnFilterDefs],
['RelatedPanel', 'opportunity', defs.buildDetailOpportunityColumnFilterDefs],
['RelatedPanel', 'contract', defs.buildDetailContractColumnFilterDefs],
['RelatedPanel', 'log', defs.buildDetailOperLogColumnFilterDefs]
] as const) {
test(`${page}/${kind}: 每个 Web 字段均绑定真实筛选状态,刷新立即移除旧行`, async () => {
const { api, stop } = mount(page, kind)
await flush()
for (const def of builder(() => [])) {
api.resetMore()
assert.equal(api.filterApi.getColumnDef(def.prop)?.type, def.type)
api.rows.value = [
{
id: 'a',
[def.prop]:
def.type === 'datetime' ? '2026-10-22 00:00:00' : def.type === 'number' ? 10 : 'alpha'
},
{
id: 'b',
[def.prop]:
def.type === 'datetime' ? '2026-10-23 23:59:59' : def.type === 'number' ? 20 : 'beta'
}
]
const state = api.filterApi.getFilterState(def.prop)
if (def.type === 'text') state.text = 'alpha'
else if (def.type === 'enum') state.selected = ['alpha']
else if (def.type === 'datetime')
Object.assign(state, { from: '2026-10-22', to: '2026-10-22' })
else Object.assign(state, { op: 'eq', value: '10' })
assert.deepEqual(
api.filtered.value.map((r: any) => r.id),
['a'],
def.prop
)
api.filterApi.clearFilter(def.prop)
assert.equal(api.filtered.value.length, 2, `${def.prop} 清空`)
}
const pending = api.load()
assert.equal(api.filtered.value.length, 0, '刷新请求未完成时已移除旧行')
await pending
assert.equal(api.filtered.value.length, 0)
stop()
})
}
// Execute the installed Vant option handler, including its same-value no-emit guard.
function clickVantOption(modelValue: unknown, value: string, update: (value: string) => void) {
const source = readFileSync(
new URL('../node_modules/vant/es/dropdown-item/DropdownItem.mjs', import.meta.url),
'utf8'
)
const ast = ts.createSourceFile('DropdownItem.mjs', source, ts.ScriptTarget.Latest, true)
let handler = ''
function visit(node: ts.Node) {
if (ts.isVariableDeclaration(node) && node.name.getText(ast) === 'onClick' && node.initializer)
handler = node.initializer.getText(ast)
ts.forEachChild(node, visit)
}
visit(ast)
assert.ok(handler, '读取真实 Vant option onClick')
const click = new Function('disabled', 'state', 'option', 'props', 'emit', `return (${handler})`)(
false,
{ showPopup: true },
{ value },
{ modelValue },
(event: string, next: string) => {
if (event === 'update:modelValue') update(next)
}
)
click()
}
for (const [page, kind, prop, quick] of [
['ContactPanel', '', 'role', 'roleFilter'],
['ContactPanel', '', 'gender', 'genderFilter'],
['InvoicePanel', '', 'isDefault', 'defaultFilter'],
['RelatedPanel', 'opportunity', 'stage', 'quick1'],
['RelatedPanel', 'opportunity', 'status', 'quick2'],
['RelatedPanel', 'contract', 'contractType', 'quick1'],
['RelatedPanel', 'contract', 'status', 'quick2'],
['RelatedPanel', 'log', 'businessType', 'quick1'],
['RelatedPanel', 'log', 'status', 'quick2']
])
test(`${page}/${kind}/${prop}: 多选后点击 Vant 全部确实清空`, async () => {
const { api, stop } = mount(page, kind)
await flush()
api.rows.value = ['a', 'b', 'c'].map((value) => ({ id: value, [prop]: value }))
api.filterApi.getFilterState(prop).selected = ['a', 'b']
assert.equal(api.filtered.value.length, 2)
clickVantOption(api[quick].value, '', (value) => {
api[quick].value = value
})
assert.equal(api.filtered.value.length, 3)
assert.equal(api.filterApi.isFilterActive(prop), false)
stop()
})
test('日期筛选年份可输入、窗口有界、旧年份可达且确认直接生效', async () => {
const definitions = defs.buildDetailContractColumnFilterDefs(() => [])
const rows = vue.ref([{ signDate: '1800-02-28' }, { signDate: '2400-02-29' }])
const filterApi = useClientColumnFilters(rows, definitions)
const { api, stop } = mount('LocalFilters', '', { filterApi, definitions })
api.openDate('signDate', 'from')
assert.ok(api.yearInput, '日期筛选应支持输入年份')
api.dateParts.value = ['2024', '02', '29']
await vue.nextTick()
api.yearInput.value = '1800'
api.onDateConfirm()
assert.equal((filterApi.getFilterState('signDate') as any).from, '1800-02-28')
assert.equal(filterApi.filteredRows.value.length, 2)
api.openDate('signDate', 'from')
assert.equal(api.pickerYear.value, 1800)
assert.equal(api.maxDate.value.getFullYear() - api.minDate.value.getFullYear(), 20)
api.yearInput.value = '2400'
api.onDateConfirm()
assert.equal(filterApi.filteredRows.value.length, 1)
api.openDate('signDate', 'to')
api.yearInput.value = '9999'
api.applyYear()
assert.equal(api.maxDate.value.getFullYear(), 9999)
api.yearInput.value = '1000'
api.applyYear()
assert.equal(api.minDate.value.getFullYear(), 1000)
stop()
})
test('关联商机直接按ID打开,目标切换和卸载丢弃迟到响应', async () => {
const pending: Record<string, (value: any) => void> = {}
const { api, props, stop } = mount('RelatedPanel', 'opportunity', { targetOpportunityId: 'O1', readonly: true }, {
getOpportunity: (id: string) => new Promise((resolve) => { pending[id] = resolve })
})
await flush()
assert.equal(api.detailOpen.value, true)
assert.ok(pending.O1, '无需列表命中即按ID读取')
props.targetOpportunityId = 'O2'
await flush()
pending.O1({ id: 'O1' })
await flush()
assert.notEqual(api.detail.value?.id, 'O1')
pending.O2({ id: 'O2' })
await flush()
assert.equal(api.detail.value.id, 'O2')
props.targetOpportunityId = 'O3'
await flush()
stop()
pending.O3({ id: 'O3' })
await flush()
assert.equal(api.detailOpen.value, false)
assert.equal(api.detail.value, undefined)
})
test('关闭关联商机取消读取,重新打开同ID可以重试', async () => {
let resolve!: (value: any) => void
const { api, props, stop } = mount('RelatedPanel', 'opportunity', { targetOpportunityId: 'O1', readonly: true }, {
getOpportunity: () => new Promise((r) => { resolve = r })
})
await flush()
assert.equal(api.detailOpen.value, true)
api.closeDetail()
resolve({ id: 'O1' })
await flush()
assert.equal(api.detail.value, undefined)
props.targetOpportunityId = ''
await flush()
props.targetOpportunityId = 'O1'
await flush()
resolve({ id: 'O1' })
await flush()
assert.equal(api.detail.value.id, 'O1')
stop()
})
function rangeHarness(kind = 'contract') {
const parent = mount('RelatedPanel', kind)
const messages: string[] = []
const child = mount('LocalFilters', '', {
definitions: parent.api.filterDefinitions.value,
filterApi: parent.api.filterApi
}, { showFailToast: (message: string) => messages.push(message) })
if (parent.api.localFilters) parent.api.localFilters.value = child.api
const edit = (prop: string, field: string, value: string) => {
if (child.api.setRangeValue) child.api.setRangeValue(prop, field, value)
else child.api.state(prop)[field] = value
}
return { parent, child, edit, messages, stop: () => { child.stop(); parent.stop() } }
}
test('关联金额倒置草稿不污染有效筛选,确定保留面板并提示,修正后可提交', async () => {
const h = rangeHarness()
try {
await flush()
const number = h.parent.api.filterDefinitions.value.find((d: any) => d.type === 'number')
const prop = number.prop
h.parent.api.rows.value = [{ id: 'a', [prop]: 100 }, { id: 'b', [prop]: 1000 }]
h.edit(prop, 'min', '200')
assert.deepEqual(h.parent.api.filtered.value.map((r: any) => r.id), ['b'])
h.edit(prop, 'max', '100')
assert.deepEqual(h.parent.api.filtered.value.map((r: any) => r.id), ['b'])
assert.equal(h.child.api.state(prop).max, '100')
h.parent.api.moreOpen.value = true
h.parent.api.applyMore()
assert.equal(h.parent.api.moreOpen.value, true)
assert.deepEqual(h.messages, [`${number.label}:最小值不能大于最大值`])
h.edit(prop, 'max', '1000')
assert.equal(h.child.api.rangeError.value, '')
h.parent.api.applyMore()
assert.equal(h.parent.api.moreOpen.value, false)
} finally { h.stop() }
})
test('所有关联日期字段拒绝倒置草稿,保留已生效日期且可修正', async () => {
for (const kind of ['contract', 'opportunity', 'log']) {
const h = rangeHarness(kind)
try {
await flush()
for (const def of h.parent.api.filterDefinitions.value.filter((d: any) => d.type === 'datetime')) {
h.child.api.clearAllFilters()
h.edit(def.prop, 'from', '2025-01-01')
h.edit(def.prop, 'to', '2024-01-01')
assert.equal(h.parent.api.filterApi.getFilterState(def.prop).to, '', `${kind}/${def.prop}`)
h.parent.api.moreOpen.value = true
h.parent.api.applyMore()
assert.equal(h.parent.api.moreOpen.value, true)
assert.equal(h.messages.at(-1), `${def.label}:开始日期不能晚于结束日期`)
h.edit(def.prop, 'to', '2025-01-01')
assert.equal(h.child.api.rangeError.value, '')
h.parent.api.applyMore()
assert.equal(h.parent.api.moreOpen.value, false)
}
} finally { h.stop() }
}
})
test('区间清空本项、全部清空及父面板重置同时清理错误草稿与生效条件', async () => {
const h = rangeHarness()
try {
await flush()
const prop = h.parent.api.filterDefinitions.value.find((d: any) => d.type === 'number').prop
for (const reset of [() => h.child.api.clearField(prop), () => h.child.api.clearAllFilters(), () => h.parent.api.resetMore()]) {
h.edit(prop, 'min', '200')
h.edit(prop, 'max', '100')
reset()
assert.equal(h.child.api.state(prop).min, '')
assert.equal(h.child.api.state(prop).max, '')
assert.equal(h.child.api.rangeError?.value || '', '')
assert.equal(h.parent.api.filterApi.hasActiveFilters.value, false)
}
} finally { h.stop() }
})
test('区间合法零值、负数、小数、单边、相等及切换比较方式继续即时生效', async () => {
const h = rangeHarness()
try {
await flush()
const prop = h.parent.api.filterDefinitions.value.find((d: any) => d.type === 'number').prop
for (const [min, max] of [['', ''], ['0', ''], ['', '0'], ['0', '0'], ['-2.5', '-1.5'], ['44.99', '44.99']]) {
h.child.api.clearAllFilters()
h.edit(prop, 'min', min)
h.edit(prop, 'max', max)
assert.equal(h.parent.api.filterApi.getFilterState(prop).min, min)
assert.equal(h.parent.api.filterApi.getFilterState(prop).max, max)
}
for (const op of ['eq', 'gt', 'gte', 'lt', 'lte']) {
h.child.api.clearAllFilters()
h.edit(prop, 'min', '200')
h.edit(prop, 'max', '100')
h.child.api.openOp(prop)
h.child.api.onOpConfirm({ selectedValues: [op] })
h.edit(prop, 'value', '44.99')
assert.equal(h.child.api.validate(), true)
assert.equal(h.parent.api.filterApi.getFilterState(prop).op, op)
assert.equal(h.parent.api.filterApi.getFilterState(prop).value, '44.99')
h.child.api.onOpConfirm({ selectedValues: ['between'] })
assert.equal(h.child.api.validate(), false)
assert.equal(h.parent.api.filterApi.getFilterState(prop).op, op)
}
} finally { h.stop() }
})
for (const page of ['ContactPanel', 'InvoicePanel', 'RelatedPanel']) {
test(`${page}: 重置清除所有枚举搜索词并恢复选项,未挂载筛选器时仍能清除条件`, async () => {
const parent = mount(page)
await flush()
const definitions = vue.unref(parent.api.filterDefinitions)
const child = mount('LocalFilters', '', { definitions, filterApi: parent.api.filterApi })
try {
if (parent.api.localFilters) parent.api.localFilters.value = child.api
const enums = definitions.filter((d: any) => d.type === 'enum')
assert.ok(enums.length)
for (const def of enums) {
child.api.enumKeywords[def.prop] = '无此选项'
child.api.state(def.prop).selected = ['sample']
}
parent.api.resetMore()
for (const def of enums) {
assert.equal(child.api.enumKeywords[def.prop] || '', '', `${def.prop}搜索词应清空`)
assert.deepEqual(child.api.state(def.prop).selected, [])
assert.deepEqual(child.api.enumOptions(def), def.enumOptions || parent.api.filterApi.collectUniqueOptions(def.prop))
}
// 没有选中条件时再次搜索,组件内“清空筛选”也必须恢复完整选项。
child.api.enumKeywords[enums[0].prop] = '不存在'
child.api.clearAllFilters()
assert.equal(child.api.enumKeywords[enums[0].prop] || '', '')
parent.api.localFilters.value = undefined
parent.api.filterApi.getFilterState(enums[0].prop).selected = ['sample']
parent.api.resetMore()
assert.equal(parent.api.filterApi.hasActiveFilters.value, false)
} finally { child.stop(); parent.stop() }
})
}