import assert from 'node:assert/strict' import test from 'node:test' import { effectScope, reactive, nextTick } from 'vue' import { useMobileCertFacet } from '../src/views/m/pl/plan-cert/useMobileCertFacet' function harness() { const requests: any[] = [] const context = reactive({ planNoIn: 'chosen', planType: 'outbound', page: 2, sortField: 'date' }) const account = reactive({ token: 'a' }) const scope = effectScope() const api = scope.run(() => useMobileCertFacet({ defs: [{ prop: 'planNo', type: 'enum', label: '计划编号' }], context: () => context, account: () => account.token, fetch: (params: any) => new Promise((resolve, reject) => requests.push({ params, resolve, reject })) }) )! return { api, requests, context, account, scope } } const flush = async () => { await Promise.resolve() await nextTick() } test('remote keyword finds values outside first 500; strips self and paging; preserves selection and cancel', async () => { const h = harness() h.api.open('planNo', ['chosen']) assert.equal(h.requests[0].params.planNoIn, undefined) assert.equal(h.requests[0].params.page, undefined) assert.equal(h.requests[0].params.sortField, undefined) h.requests[0].resolve({ list: Array.from({ length: 500 }, (_, i) => ({ value: `${i}` })) }) await flush() h.api.search('PO20261009022') assert.equal(h.requests[1].params.keyword, 'PO20261009022') h.requests[1].resolve({ data: { list: [{ value: 'PO20261009022' }] } }) await flush() assert.equal(h.api.state.options[0].value, 'PO20261009022') h.api.toggle('PO20261009022') h.api.search('no result') h.requests[2].resolve({ list: [] }) await flush() assert.deepEqual(h.api.confirm(), { field: 'planNo', selected: ['chosen', 'PO20261009022'] }) h.api.open('planNo', ['chosen']) h.api.toggle('changed') h.api.close() assert.equal(h.api.confirm(), null) h.api.open('planNo', ['chosen']) assert.deepEqual(h.api.state.selected, ['chosen']) h.scope.stop() }) test('reverse query success/error/finally cannot replace active request; retry is distinct from empty', async () => { const h = harness() h.api.open('planNo', []) h.api.search('new') h.requests[0].reject(new Error('stale')) await flush() assert.equal(h.api.state.error, '') assert.equal(h.api.state.loading, true) h.requests[1].reject(new Error('network')) await flush() assert.equal(h.api.state.error, 'network') assert.equal(h.api.state.loading, false) h.api.retry() h.requests[2].resolve({ list: [] }) await flush() assert.equal(h.api.state.error, '') assert.deepEqual(h.api.state.options, []) h.api.search('older') h.api.search('latest') h.requests[4].resolve({ list: [{ value: 'latest' }] }) await flush() h.requests[3].resolve({ list: [{ value: 'older' }] }) await flush() assert.equal(h.api.state.options[0].value, 'latest') h.scope.stop() }) test('close/reopen, context, account and disposal invalidate requests and confirmation', async () => { const h = harness() h.api.open('planNo', ['a']) h.api.close() h.api.open('planNo', ['b']) h.requests[0].resolve({ list: [{ value: 'old' }] }) await flush() assert.deepEqual(h.api.state.options, []) assert.equal(h.api.state.loading, true) h.context.planType = 'inbound' assert.equal(h.api.confirm(), null) h.requests[1].reject(new Error('stale')) await flush() assert.equal(h.api.state.error, '') h.api.open('planNo', ['b']) h.account.token = 'b' assert.equal(h.api.confirm(), null) h.api.open('planNo', ['c']) h.scope.stop() h.requests[3].resolve({ list: [{ value: 'after disposal' }] }) await flush() assert.equal(h.api.confirm(), null) assert.deepEqual(h.api.state.options, []) }) // Execute the actual SFC setup with Vue reactivity; only transport/store/UI effects are substituted. import { readFileSync } from 'node:fs' import { parse } from '@vue/compiler-sfc' import ts from 'typescript' import * as vue from 'vue' import { useServerColumnFilters } from '../src/composables/useServerColumnFilters' import { plPlanCertColumnFilterDefs as defs } from '../src/views/pl/plan-cert/columnFilterDefs' import { withPlanContractIoFacetOptions, PL_PLAN_CONTRACT_SIDE_FILTER_OPTIONS } from '../src/utils/pl-plan-contract-display' import { applyMergedCustomerQueryParams, applyContractSideQueryParams } from '../src/utils/pl-plan-pair-list-query' import { PL_PLAN_DEFAULT_FOLLOW_STATUSES, plPlanCertTabToFilters } from '../src/utils/pl-display' import { buildQuery, facetPlanDateParams } from '../src/views/m/pl/plan-cert/model' function component() { const scope = effectScope() const requests: any[] = [], events: any[] = [], warnings: string[] = [] const props = reactive({ modelValue: false, filters: {} as any, sort: { sortField: '', sortOrder: 'desc' }, context: { planType: 'outbound', certUploadStatus: 'incomplete', planDateFrom: '2026-10-01', followStatus: 'accepted' } as any }) const source = parse( readFileSync(new URL('../src/views/m/pl/plan-cert/CertFilters.vue', import.meta.url), 'utf8') ).descriptor.scriptSetup!.content const ast = ts.createSourceFile('setup.ts', source, ts.ScriptTarget.Latest, true) const code = ts.transpile( ast.statements .filter((n) => !ts.isImportDeclaration(n)) .map((n) => n.getText(ast)) .join('\n'), { target: ts.ScriptTarget.ESNext } ) const deps = { ref: vue.ref, computed: vue.computed, watch: vue.watch, defs, useServerColumnFilters, useMobileCertFacet, withPlanContractIoFacetOptions, PL_PLAN_CONTRACT_SIDE_FILTER_OPTIONS, applyMergedCustomerQueryParams, applyContractSideQueryParams, PL_PLAN_DEFAULT_FOLLOW_STATUSES, facetPlanDateParams, useUserStore: () => reactive({ accessToken: 'a', getUserInfo: { tenantNo: 'a' } }), fetchPlanColumnFacetValues: (params: any) => new Promise((resolve, reject) => requests.push({ params, resolve, reject })), defineProps: () => props, defineEmits: () => (...args: any[]) => events.push(args), ElMessage: { warning: (v: string) => warnings.push(v) } } const api = scope.run(() => new Function( ...Object.keys(deps), `${code}; return {picker,draft,apply,resetAndApply,confirmPicker,optionLabel,pickerOptions}` )(...Object.values(deps)) ) return { api, props, requests, events, warnings, scope } } test('component preserves query semantics and confirmation is isolated until accepted', async () => { const h = component() h.props.modelValue = true await nextTick() Object.assign(h.api.draft, { followStatus: { selected: ['accepted'], text: '' }, planType: { selected: ['inbound'], text: '' }, hazardCertReady: { selected: ['1'], text: '' }, productContract: { selected: ['load', 'C-1'], text: '' } }) h.api.picker.open('planNo', []) const q = h.requests[0].params assert.equal(q.planType, 'outbound') assert.equal(q.hazardCertReady, 0) assert.equal(q.hazardCertReadyIn, undefined) assert.equal(q.followStatus, 'accepted') assert.equal(q.planDateFrom, '2026-10-01') assert.equal(q.stopKindIn, 'load') h.api.picker.toggle('P-1') assert.equal(h.api.draft.planNo, undefined) h.api.confirmPicker() assert.deepEqual(h.api.draft.planNo.selected, ['P-1']) h.api.picker.open('productContract', ['load', 'C-1']) h.api.picker.search('no match') assert.equal(h.api.optionLabel('load'), '供') assert.equal(h.api.optionLabel('C-1'), 'C-1') h.scope.stop() }) test('component date/number validation, normal apply, reset and closing discard picker', async () => { const h = component() h.props.modelValue = true await nextTick() h.api.draft.planDate = { from: '2026-10-02', to: '2026-10-01' } h.api.apply() assert.equal(h.events.length, 0) assert.match(h.warnings[0], /开始不能晚于结束/) delete h.api.draft.planDate const number = defs.find((d) => d.type === 'number')! h.api.draft[number.prop] = { op: 'between', min: '9', max: '2' } h.api.apply() assert.equal(h.events.length, 0) assert.match(h.warnings[1], /最小值不能大于最大值/) h.api.draft[number.prop] = { op: 'between', min: '2', max: '9' } h.api.apply() assert.equal(h.events[0][0], 'apply') h.api.resetAndApply() assert.deepEqual(h.events[2][1].followStatus.selected, PL_PLAN_DEFAULT_FOLLOW_STATUSES) assert.equal(h.events[2][3], true) h.api.picker.open('planNo', []) h.props.modelValue = false await nextTick() assert.equal(h.api.picker.confirm(), null) h.scope.stop() }) test('component independent customer sides, contract-side stripping and draft date replacement use real query helpers', async () => { const h = component() h.props.modelValue = true await nextTick() Object.assign(h.api.draft, { loadCustomer: { selected: ['装货甲'], text: '' }, deliveryCustomer: { selected: ['送货乙'], text: '' }, productContract: { selected: ['load', 'C-1'], text: '' }, planDate: { from: '2026-10-08', to: '2026-10-09' } }) h.api.picker.open('planNo', []) assert.equal(h.requests[0].params.loadCustomersIn, '装货甲') assert.equal(h.requests[0].params.deliveryCustomersIn, '送货乙') assert.equal(h.requests[0].params.stopCustomers, undefined) assert.equal(h.requests[0].params.contractNoIn, 'C-1') assert.equal(h.requests[0].params.planDateFrom, '2026-10-08') h.api.picker.open('loadCustomer', []) assert.equal(h.requests[1].params.stopCustomers, undefined) h.api.picker.open('productContract', ['load', 'C-1']) assert.equal(h.requests[2].params.stopKindIn, undefined) assert.equal(h.requests[2].params.contractNoIn, undefined) assert.deepEqual( h.api.pickerOptions.value.map((o: any) => o.label), ['供', '需', '出库', '入库'] ) h.scope.stop() }) test('confirmed contract summary retains side labels when another picker is active', async () => { const h = component() h.props.modelValue = true await nextTick() h.api.picker.open('productContract', ['load', 'delivery', 'C-1', '出库']) h.api.confirmPicker() h.api.picker.open('planNo', []) const labels = h.api.draft.productContract.selected.map((value: string) => h.api.optionLabel(value, 'productContract') ) assert.deepEqual(labels, ['供', '需', 'C-1', '出库']) assert.equal(h.api.optionLabel('load', 'planNo'), 'load') h.scope.stop() }) import { Search as VanSearch } from 'vant' test('actual Vant Search mounts a native clear button whose activation preserves picker and selection', async () => { const h = component() h.props.modelValue = true await nextTick() h.api.picker.open('planNo', ['kept']) h.api.picker.search('PO20261021002') const source = parse( readFileSync(new URL('../src/views/m/pl/plan-cert/CertFilters.vue', import.meta.url), 'utf8') ).descriptor.template!.content const searchTemplate = source.match(/|<\/van-search>)/)![0] type Node = { type: string; props: any; children: Node[]; parent?: Node } const renderer = vue.createRenderer({ createElement: (type) => ({ type, props: {}, children: [] }), createText: (text) => ({ type: '#text', props: { text }, children: [] }), createComment: () => ({ type: '#comment', props: {}, children: [] }), setText: (node, text) => { node.props.text = text }, setElementText: (node, text) => { node.props.text = text node.children = [] }, parentNode: (node) => node.parent || null, nextSibling: () => null, patchProp: (node, key, _old, value) => { node.props[key] = value }, insert: (node, parent) => { node.parent = parent parent.children.push(node) }, remove: (node) => { if (node.parent) node.parent.children = node.parent.children.filter((n) => n !== node) } }) const root: Node = { type: 'root', props: {}, children: [] } const app = renderer.createApp({ components: { VanSearch }, setup: () => ({ picker: h.api.picker, pickerState: h.api.picker.state, pickerTitle: '计划编号' }), render: vue.compile(searchTemplate) }) app.mount(root) await nextTick() const find = (node: Node): Node | undefined => node.type === 'button' && node.props['aria-label'] === '清空搜索' ? node : node.children.map(find).find(Boolean) const clear = find(root) assert.ok( clear, 'Search must expose a real button usable by mouse, touch-generated click and keyboard' ) // Touch generates click too; both interaction paths use the same accessible native control. for (const pointerType of ['mouse', 'touch', 'keyboard']) { h.api.picker.search('PO20261021002') await nextTick() const activeClear = find(root)! assert.ok(activeClear) if (pointerType === 'mouse') { let prevented = false activeClear.props.onMousedown({ preventDefault() { prevented = true } }) assert.equal(prevented, true) } activeClear.props.onClick({ type: 'click', pointerType, stopPropagation() {}, preventDefault() {} }) await nextTick() assert.equal(h.api.picker.state.keyword, '') assert.equal(find(root), undefined) assert.equal(h.api.picker.state.visible, true) assert.deepEqual(h.api.picker.state.selected, ['kept']) assert.equal(h.requests.at(-1).params.keyword, '') } app.unmount() h.scope.stop() }) test('contract search filters static IO choices and has a true empty state', async () => { const h = component() h.props.modelValue = true await nextTick() h.api.picker.open('productContract', ['load']) h.api.picker.search('zz-no-contract') h.requests.at(-1).resolve({ list: [] }) await nextTick() await flush() assert.deepEqual(h.api.pickerOptions.value, []) assert.deepEqual(h.api.picker.state.selected, ['load']) h.api.picker.search('入库') assert.deepEqual( h.api.pickerOptions.value.map((o: any) => o.label), ['入库'] ) h.scope.stop() }) for (const tab of ['all', 'cert_incomplete', 'inbound', 'outbound']) { test(`certificate ${tab} list query matches Web for independent customers and compound filters`, () => { const { filters, toQueryParams } = useServerColumnFilters(defs) Object.assign(filters, { followStatus: { text: '', selected: ['accepted'] }, loadCustomer: { text: '', selected: ['装货甲'] }, deliveryCustomer: { text: '', selected: ['送货乙'] }, productContract: { text: '', selected: ['load', 'C-1'] }, hazardCertReady: { text: '', selected: ['1'] } }) const sort = { sortField: 'planDate', sortOrder: 'desc' } const env = { filters, toQueryParams, sort, buildQuery, plPlanCertTabToFilters, applyMergedCustomerQueryParams, applyContractSideQueryParams, tab: vue.ref(tab), activePlanTypeTab: vue.ref(tab), appliedKeyword: vue.ref(' 运输 '), quickSearch: vue.ref(' 运输 '), dates: vue.ref(['2026-10-01', '2026-10-24']), dateRange: vue.ref(['2026-10-01', '2026-10-24']), toSortQueryParams: () => sort } const evaluate = (path: string, name: string) => { const ast = ts.createSourceFile( 'query.ts', parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const fn = ast.statements.find((n) => ts.isFunctionDeclaration(n) && n.name?.text === name)! return new Function( ...Object.keys(env), `${ts.transpile(fn.getText(ast), { target: ts.ScriptTarget.ESNext })};return ${name}()` )(...Object.values(env)) } const mobile = evaluate('../src/views/m/pl/plan-cert/index.vue', 'query') assert.deepEqual( mobile, evaluate('../src/views/pl/plan-cert/index.vue', 'buildListQueryParams') ) assert.equal(mobile.loadCustomersIn, '装货甲') assert.equal(mobile.deliveryCustomersIn, '送货乙') }) }