Files
shanxia2.0/web-admin/scripts/mobile-customer-columns.test.ts

277 lines
9.5 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { existsSync, readFileSync } from 'node:fs'
import { nextTick, effectScope } from 'vue'
const file = new URL('../src/views/m/crm/customer/columns.ts', import.meta.url)
async function columns() {
assert.ok(existsSync(file), 'mobile customer column configuration must exist')
return import('../src/views/m/crm/customer/columns')
}
test('35 optional columns and 14 defaults exactly match Web', async () => {
const c = await columns()
const web = readFileSync(new URL('../src/views/crm/customer/index.vue', import.meta.url), 'utf8')
const defs = web.match(/const listColumnDefs = (\[[\s\S]*?\n\])/)![1]
const defaults = web.match(/defaultVisibleProps: (\[[\s\S]*?\])/)![1]
assert.deepEqual(c.customerColumnDefs, new Function(`return ${defs}`)())
assert.deepEqual(c.defaultCustomerColumns, new Function(`return ${defaults}`)())
assert.equal(c.customerColumnDefs.length, 35)
assert.equal(c.defaultCustomerColumns.length, 14)
})
test('hide clears enum/text/numeric/date aliases and paired sort, preserves context and identity', async () => {
const c = await columns()
const query = {
customerNoIn: 'C1',
customerNameIn: '客户',
mobile: '123',
levelIn: 'A',
followCount: '2',
followCountOp: 'gte',
followCountMin: '1',
followCountMax: '3',
beginTime: '2026-01-01',
endTime: '2026-02-01',
sortField: 'createTime',
sortOrder: 'asc',
keyword: '搜索',
viewMode: 'mine'
}
assert.deepEqual(c.cleanCustomerColumnQuery(query, []), {
customerNoIn: 'C1',
customerNameIn: '客户',
keyword: '搜索',
viewMode: 'mine'
})
assert.deepEqual(c.cleanCustomerColumnQuery(query, ['createTime'], true), {
keyword: '搜索',
viewMode: 'mine'
})
assert.equal(query.mobile, '123')
})
test('customer list ignores obsolete field preferences and always shows Web defaults', async () => {
const c = await columns()
const reads: string[] = []
Object.assign(globalThis, {
localStorage: {
getItem: (key: string) => {
reads.push(key)
return '[]'
},
setItem: () => {}
}
})
const h = await harness('index')
try {
assert.deepEqual(h.api.visibleColumns.value, c.defaultCustomerColumns)
assert.deepEqual(reads, [], 'obsolete hidden-column preferences must not be read')
for (const name of ['selectMode', 'selected', 'columnsOpen', 'poolOpen'])
assert.equal(h.api[name], undefined, `${name} must not remain in the mobile list`)
} finally {
h.stop()
delete (globalThis as any).localStorage
}
})
import { parse } from '@vue/compiler-sfc'
import * as Vue from 'vue'
import ts from 'typescript'
import * as model from '../src/views/m/crm/customer/model'
import { buildCustomerColumnFilterDefs } from '../src/views/crm/customer/columnFilterDefs'
async function harness(page: string, props: any = {}) {
const c = await columns()
const source = readFileSync(
new URL(`../src/views/m/crm/customer/${page}.vue`, import.meta.url),
'utf8'
)
const script = parse(source).descriptor.scriptSetup!.content
const ast = ts.createSourceFile('page.ts', script, ts.ScriptTarget.Latest, true)
const statements = ast.statements.filter((s) => !ts.isImportDeclaration(s))
const names = statements.flatMap((s) =>
ts.isFunctionDeclaration(s) && s.name
? [s.name.text]
: ts.isVariableStatement(s)
? s.declarationList.declarations.flatMap((d) =>
ts.isIdentifier(d.name)
? [d.name.text]
: ts.isObjectBindingPattern(d.name)
? d.name.elements.map((e) => e.name.getText(ast))
: []
)
: []
)
const bindings: Record<string, any> = { ...Vue }
for (const s of ast.statements)
if (ts.isImportDeclaration(s)) {
if (s.importClause?.name) bindings[s.importClause.name.text] = () => {}
const n = s.importClause?.namedBindings
if (n && ts.isNamedImports(n)) for (const e of n.elements) bindings[e.name.text] = () => {}
}
const requests: any[] = [],
events: any[] = [],
navigations: string[] = []
Object.assign(bindings, model, c, {
buildCustomerColumnFilterDefs,
defineProps: () => props,
defineEmits:
() =>
(...args: any[]) =>
events.push(args),
useRouter: () => ({ push: (path: string) => navigations.push(path) }),
useDictStore: () => ({ getOptions: () => [], getLabel: (_: any, v: any) => v }),
useListReturnState: () => ({ restoring: Vue.ref(false) }),
useIntersectionObserver: () => {},
useDebounceFn: (fn: any) => fn,
fetchCustomerList: async (q: any) => {
requests.push(q)
return { rows: [], total: 0 }
},
onMounted: () => {},
onBeforeUnmount: () => {}
})
for (const key of Object.keys(bindings))
if (!/^[a-zA-Z_$][\w$]*$/.test(key) || key === 'default') delete bindings[key]
const code = ts.transpile(statements.map((s) => s.getText(ast)).join('\n'), {
target: ts.ScriptTarget.ESNext
})
const scope = effectScope()
const api = scope.run(() =>
new Function(...Object.keys(bindings), `${code}; return {${names.join(',')}}`)(
...Object.values(bindings)
)
)
return { api, requests, events, navigations, source, stop: () => scope.stop() }
}
test('fixed customer defaults retain search filters and remove stale non-default filters', async () => {
const h = await harness('index')
try {
assert.ok(h.api.previewFields.value.length)
assert.match(
h.api.cardMeta({ customerNo: 'C1', status: 'normal', createTime: '2026' }),
/C1.*2026/
)
h.api.query.value = { levelIn: 'A', email: 'old hidden field', customerNoIn: 'C1' }
await h.api.fetchPage(true)
assert.equal(h.requests.at(-1).levelIn, 'A')
assert.equal(h.requests.at(-1).customerNoIn, 'C1')
assert.equal(h.requests.at(-1).email, undefined)
} finally {
h.stop()
}
})
test('filter definitions and drafts track visibility, discarded draft fields never return', async () => {
const props = Vue.reactive({
modelValue: false,
query: { levelIn: 'A', mobile: '123' },
context: {},
visibleColumns: ['level', 'mobile']
})
const h = await harness('CustomerFilters', props)
try {
props.modelValue = true
await nextTick()
await nextTick()
assert.deepEqual(
h.api.defs.value.map((d: any) => d.prop),
['customerNo', 'customerName', 'mobile', 'level']
)
props.visibleColumns = []
await nextTick()
await nextTick()
assert.deepEqual(h.api.buildQuery(), {})
props.visibleColumns = ['level', 'mobile']
await nextTick()
await nextTick()
assert.deepEqual(h.api.buildQuery(), {}, 'stale props must not rehydrate hidden draft filters')
} finally {
h.stop()
}
})
test('unchanged effective filter submissions do not reload the list', async () => {
const h = await harness('index')
try {
h.api.applyFilters({ query: {}, keyword: '', viewMode: 'all' })
assert.equal(h.requests.length, 0)
h.api.applyFilters({ query: { mobile: '123' }, keyword: '', viewMode: 'all' })
assert.equal(h.requests.length, 1)
h.api.applyFilters({ query: { mobile: '123', email: 'hidden' }, keyword: '', viewMode: 'all' })
assert.equal(h.requests.length, 1)
} finally {
h.stop()
}
})
test('actual customer list keeps default business data and omits desktop management controls', async () => {
const { compileTemplate } = await import('@vue/compiler-sfc')
const { renderToString } = await import('vue/server-renderer')
const h = await harness('index')
try {
h.api.rows.value = [
{
id: '1',
customerName: '始终名称',
customerNo: '始终编号',
mobile: '手机号值',
status: '状态值',
createTime: '创建时间值',
email: '邮箱值'
}
]
h.api.loading.value = false
const template = compileTemplate({
id: 'list',
filename: 'index.vue',
source: parse(h.source).descriptor.template!.content
})
assert.deepEqual(template.errors, [])
const exports: any = {}
const stub = {
setup:
(_: any, { slots }: any) =>
() =>
Vue.h(
'div',
Object.values(slots).flatMap((slot: any) => slot())
)
}
const require = () => ({ ...Vue, resolveComponent: () => stub })
new Function(
'require',
'exports',
ts.transpile(template.code, {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ESNext
})
)(require, exports)
const render = () =>
renderToString(
Vue.createSSRApp({ setup: () => () => exports.render(Vue.proxyRefs(h.api), []) })
)
const rendered = await render()
for (const value of ['始终名称', '始终编号', '手机号值', '状态值', '创建时间值'])
assert.ok(rendered.includes(value), value)
for (const value of [
'邮箱值',
'刷新列表',
'字段设置',
'批量选择',
'清空筛选',
'列统计',
'放入公海'
])
assert.ok(!rendered.includes(value), value)
assert.match(rendered, /新增客户/)
assert.doesNotMatch(
rendered,
/<template(?:\s|>)/,
'create action must not be inside an inert template'
)
h.api.goNew()
assert.deepEqual(h.navigations, ['/m/crm/customer/new'])
h.api.rows.value = []
assert.match(await render(), /新增客户/, 'empty lists must allow creating the first customer')
h.api.forbidden.value = true
assert.doesNotMatch(await render(), /新增客户/, 'forbidden lists must hide the create action')
} finally {
h.stop()
}
})