Files
shanxia2.0/web-admin/scripts/mobile-customer-filter-dates.test.ts
T

348 lines
12 KiB
TypeScript

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<string, string> = {}) {
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(/<li\b/g) || []).length
assert.ok(count > 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()
}
})