import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import * as Vue from 'vue' import { renderToString } from 'vue/server-renderer' import { DatePicker } from 'vant' import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc' import ts from 'typescript' import * as columns from '../src/views/m/crm/customer/columns' import * as model from '../src/views/m/crm/customer/model' import * as definitions from '../src/views/crm/customer/columnFilterDefs' function harness(query: Record = {}) { const messages: string[] = [] let liveApply: () => void = () => {} const file = 'CustomerFilters.vue' const { descriptor } = parse( readFileSync(new URL(`../src/views/m/crm/customer/${file}`, import.meta.url), 'utf8') ) 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, []) const require = (name: string) => name === 'vue' ? { ...Vue, resolveComponent: (name: string) => name } : name.includes('/dict') ? { useDictStore: () => ({ getOptions: () => [] }) } : name.includes('columnFilterDefs') ? definitions : name === './columns' ? columns : name === './model' ? model : name === '@vueuse/core' ? { useDebounceFn: (callback: () => void) => { liveApply = callback return () => {} } } : name === 'vant' ? { showFailToast: (message: string) => messages.push(message) } : { default: 'MpStructSkeleton' } function evaluate(source: string) { const exports: any = {} new Function( 'require', 'exports', 'ref', 'computed', 'watch', 'nextTick', ts.transpile(source, { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ESNext }) )(require, exports, Vue.ref, Vue.computed, Vue.watch, Vue.nextTick) return exports } const props = Vue.reactive({ modelValue: true, query, context: {} }) const emitted: any[] = [] const scope = Vue.effectScope() const setup = scope.run(() => evaluate(script.content).default.setup(props, { expose() {}, emit: (...args: any[]) => emitted.push(args) }) ) setup.syncFromProps() const render = evaluate(template.code).render function nodes() { const found: any[] = [] function visit(node: any) { if (!node || typeof node !== 'object') return if (Array.isArray(node)) return node.forEach(visit) found.push(node) if (Array.isArray(node.children)) visit(node.children) else if (node.children && typeof node.children === 'object') for (const slot of Object.values(node.children)) if (typeof slot === 'function') visit(slot()) } visit(render({}, [], props, Vue.proxyRefs(setup), {}, {})) return found } return { setup, emitted, nodes, props, messages, runLive: () => liveApply(), stop: () => scope.stop() } } test('every reversed numeric range blocks explicit and live application without losing the draft', async () => { for (const def of definitions .buildCustomerColumnFilterDefs(() => []) .filter((d) => d.type === 'number')) { const h = harness() try { await Vue.nextTick() h.setup.draft.value[def.prop + 'Min'] = '10' h.setup.draft.value[def.prop + 'Max'] = '2' h.runLive() h.setup.apply() assert.deepEqual(h.emitted, [], def.prop + ': invalid range must not submit or close') assert.equal(h.setup.draft.value[def.prop + 'Min'], '10') assert.equal(h.setup.draft.value[def.prop + 'Max'], '2') assert.equal(h.messages.at(-1), def.label + ':最小值不能大于最大值') assert.ok(h.nodes().some((node) => node.type === 'p' && node.props?.role === 'alert')) } finally { h.stop() } } }) test('every reversed date range blocks explicit and live application while retaining both dates', async () => { for (const def of definitions .buildCustomerColumnFilterDefs(() => []) .filter((d) => d.type === 'datetime')) { const h = harness() try { await Vue.nextTick() const from = def.queryKeyFrom || def.prop + 'From' const to = def.queryKeyTo || def.prop + 'To' h.setup.draft.value[from] = '2026-10-24' h.setup.draft.value[to] = '2026-10-23' h.runLive() h.setup.apply() assert.deepEqual(h.emitted, [], def.prop + ': invalid range must not submit or close') assert.equal(h.setup.draft.value[from], '2026-10-24') assert.equal(h.setup.draft.value[to], '2026-10-23') assert.equal(h.messages.at(-1), def.label + ':开始日期不能晚于结束日期') } finally { h.stop() } } }) test('valid equal, single-sided, zero and decimal ranges remain usable; inactive bounds are ignored', async () => { const h = harness() try { await Vue.nextTick() for (const [min, max] of [ ['', ''], ['0', ''], ['', '2'], ['0', '0'], ['0.1', '0.2'], ['-2', '-1'] ]) { h.emitted.length = 0 Object.assign(h.setup.draft.value, { dealAmountOp: 'between', dealAmountMin: min, dealAmountMax: max }) h.setup.apply() assert.ok( h.emitted.some(([event]) => event === 'apply'), `${min}..${max}` ) } for (const op of ['eq', 'gt', 'gte', 'lt', 'lte']) { h.emitted.length = 0 Object.assign(h.setup.draft.value, { dealAmountOp: op, dealAmount: '0', dealAmountMin: '2', dealAmountMax: '1' }) h.setup.apply() assert.deepEqual(h.emitted.find(([event]) => event === 'apply')[1].query, { dealAmountOp: op, dealAmount: '0' }) } for (const [from, to] of [ ['', ''], ['2026-10-24', ''], ['', '2026-10-24'], ['2026-10-24', '2026-10-24'] ]) { h.emitted.length = 0 Object.assign(h.setup.draft.value, { createTimeFrom: from, createTimeTo: to }) h.setup.apply() assert.ok( h.emitted.some(([event]) => event === 'apply'), `${from}..${to}` ) } assert.deepEqual(h.messages, []) } finally { h.stop() } }) test('correcting a reversed range clears its message and resumes live filtering; closed panels stay inactive', async () => { const h = harness() try { await Vue.nextTick() Object.assign(h.setup.draft.value, { followCountMin: '2', followCountMax: '1', mobile: '199' }) h.setup.apply() assert.deepEqual(h.emitted, []) h.setup.draft.value.followCountMax = '2' h.runLive() assert.deepEqual(h.emitted.find(([event]) => event === 'apply')[1].query, { mobile: '199', followCountOp: 'between', followCountMin: '2', followCountMax: '2' }) assert.ok(!h.nodes().some((node) => node.type === 'p' && node.props?.role === 'alert')) h.emitted.length = 0 h.props.modelValue = false h.runLive() assert.deepEqual(h.emitted, []) } finally { h.stop() } }) test('date range selection sends Web date-only inclusive boundaries, without time controls', () => { const h = harness() try { h.setup.openDateTime('nextTimeFrom') h.setup.dateValue.value = ['2026', '10', '23'] h.setup.onDateConfirm() assert.equal(h.setup.draft.value.nextTimeFrom, '2026-10-23') h.setup.openDateTime('nextTimeTo') h.setup.dateValue.value = ['2026', '10', '23'] h.setup.onDateConfirm() assert.equal(h.nodes().filter((n) => n.type === 'van-time-picker').length, 0) assert.deepEqual(h.setup.buildQuery(), { nextTimeFrom: '2026-10-23', nextTimeTo: '2026-10-23' }) h.setup.apply() assert.deepEqual(h.emitted.find(([event]) => event === 'apply')[1].query, { nextTimeFrom: '2026-10-23', nextTimeTo: '2026-10-23' }) } finally { h.stop() } }) test('hydrated legacy date-times normalize to date-only for every defined date boundary', () => { const defs = definitions .buildCustomerColumnFilterDefs(() => []) .filter((d) => d.type === 'datetime') const query = Object.fromEntries( defs .flatMap((d) => [d.queryKeyFrom || d.prop + 'From', d.queryKeyTo || d.prop + 'To']) .map((key) => [key, '1985-02-03 12:34:56']) ) const h = harness(query) try { for (const key of Object.keys(query)) { assert.equal(h.setup.draft.value[key], '1985-02-03') assert.equal(h.setup.buildQuery()[key], '1985-02-03') } assert.equal(h.setup.displayDateTime('1985-02-03T12:34:56'), '1985-02-03') } finally { h.stop() } }) test('each date boundary exposes a clear button that preserves its opposite and other filters', () => { const h = harness({ nextTimeFrom: '2026-10-01', nextTimeTo: '2026-10-23', mobile: '138' }) try { for (const suffix of ['From', 'To']) { const label = suffix === 'From' ? '清空下次联系时间起始日期' : '清空下次联系时间截止日期' const button = h .nodes() .find((n) => n.type === 'van-button' && n.props?.['aria-label'] === label) assert.ok(button, label) button.props.onClick({ stopPropagation() {} }) assert.equal(h.setup.buildQuery()['nextTime' + suffix], undefined) assert.equal(h.setup.buildQuery().mobile, '138') assert.equal(h.setup.dateOpen.value, false) if (suffix === 'From') assert.equal(h.setup.buildQuery().nextTimeTo, '2026-10-23') } } finally { h.stop() } }) test('historical and distant filter dates are reachable with bounded real Vant rendering', async () => { const h = harness({ nextTimeFrom: '1850-03-02' }) try { h.setup.openDateTime('nextTimeFrom') assert.deepEqual(h.setup.dateValue.value, ['1850', '03', '02']) for (const year of ['1000', '1850', '2101', '9999']) { const input = h.nodes().find((n) => n.type === 'van-field' && n.props.label === '年份') assert.ok(input, 'full year range available via year input') input.props['onUpdate:modelValue'](year) input.props.onBlur() await Vue.nextTick() const picker = h.nodes().find((n) => n.type === 'van-date-picker') const html = await renderToString( Vue.createSSRApp({ render: () => Vue.h(DatePicker, { modelValue: h.setup.dateValue.value, minDate: picker.props['min-date'], maxDate: picker.props['max-date'] }) }) ) const count = (html.match(/ 0 && count <= 64, `actual Vant option count: ${count}`) h.setup.onDateConfirm() assert.equal(h.setup.buildQuery().nextTimeFrom, `${year}-03-02`) h.setup.openDateTime('nextTimeFrom') } } finally { h.stop() } }) test('cancel leaves the range intact; year edits handle leap days and incomplete input', async () => { const h = harness({ nextTimeFrom: '2000-02-29', nextTimeTo: '2026-10-23' }) try { h.setup.openDateTime('nextTimeFrom') h.setup.yearInput.value = '1900' h.setup.applyYear() await Vue.nextTick() assert.deepEqual(h.setup.dateValue.value, ['1900', '02', '28']) h.nodes() .find((n) => n.type === 'van-date-picker') .props.onCancel() assert.equal(h.setup.buildQuery().nextTimeFrom, '2000-02-29') h.setup.openDateTime('nextTimeFrom') assert.deepEqual(h.setup.dateValue.value, ['2000', '02', '29']) for (const input of ['', '999', '10000', 'abcd']) { h.setup.yearInput.value = input h.setup.applyYear() assert.equal(h.setup.yearInput.value, '2000') assert.deepEqual(h.setup.dateValue.value, ['2000', '02', '29']) } h.setup.onDateConfirm() assert.deepEqual(h.setup.buildQuery(), { nextTimeFrom: '2000-02-29', nextTimeTo: '2026-10-23' }) } finally { h.stop() } })