| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- 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\b[\s\S]*?(?:\/>|<\/van-search>)/)![0]
- type Node = { type: string; props: any; children: Node[]; parent?: Node }
- const renderer = vue.createRenderer<Node, Node>({
- 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, '送货乙')
- })
- }
|