| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 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()
- }
- })
|