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, 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 = 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({ 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>({}) 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 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() } }) }