import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import ts from 'typescript' import { parse } from '@vue/compiler-sfc' import { ref } from 'vue' import { formatBrandConstraintLabel } from '../src/utils/crm-product-brand' import { useServerColumnFilters } from '../src/composables/useServerColumnFilters' import { plPlanColumnFilterDefs } from '../src/views/pl/plan/columnFilterDefs' import { plPlanListTabToFilters } from '../src/utils/pl-display' import { applyMergedCustomerQueryParams, applyContractSideQueryParams } from '../src/utils/pl-plan-pair-list-query' import { facetPlanDateParams } from '../src/views/m/pl/plan-cert/model' function productLine() { const source = ts.createSourceFile( 'card.ts', readFileSync(new URL('../src/utils/mp-pl-plan-card.ts', import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true ) const names = [ 'formatStopProductLine', 'resolveLinkedPlanProduct', 'formatQty', 'resolveUnitLabel' ] const code = ts.transpile( source.statements .filter((node) => ts.isFunctionDeclaration(node) && names.includes(node.name!.text)) .map((node) => node.getText(source)) .join('\n'), { target: ts.ScriptTarget.ESNext } ) const env = { formatBrandConstraintLabel, resolveProductHazardFlags: () => ({ isToxic: false, isExplosive: false }), getPlanProductActualQuantity: () => '0', resolveMpPackageSpecLabel: (v: string) => v || '—', resolveMpProductUnitLabel: (v: string) => v || '—' } return new Function(...Object.keys(env), `${code};return formatStopProductLine`)( ...Object.values(env) ) } test('mobile list product brands and aliases follow the exact plan line before product ID fallback', () => { const format = productLine() const plan = { products: [ { id: 'first', productId: 'p', productName: '标准名称', brandMode: 'required', brandName: '品牌甲', aliasName: '别名甲' }, { id: 'second', productId: 'p', productName: '标准名称', brandMode: 'required', brandName: '品牌乙', aliasName: ' 别名乙 ' } ] } const second = format( plan, { kind: 'load' }, { productId: 'p', planProductId: 'second', productName: '段上别名', planQty: 2 } ) assert.equal(second.name, '标准名称') assert.equal(second.brand, '品牌乙') assert.equal(second.alias, '别名乙') assert.equal(second.planQty, '2') const fallback = format(plan, { kind: 'delivery' }, { productId: 'p' }) assert.equal(fallback.brand, '品牌甲') assert.equal(fallback.alias, '别名甲') }) test('mobile product metadata uses the Web brand constraint label and empty placeholders', () => { const format = productLine() for (const brandMode of ['any', 'required', undefined]) { const product = { id: 'line', productId: 'p', brandMode, brandName: '', aliasName: '' } const line = format({ products: [product] }, { kind: 'load' }, { planProductId: 'line' }) assert.equal(line.brand, formatBrandConstraintLabel(brandMode, '')) assert.equal(line.alias, '—') } const missing = format({ products: [] }, { kind: 'load' }, { productId: 'missing' }) assert.equal(missing.brand, '—') assert.equal(missing.alias, '—') }) for (const metadata of [ { aliasName: '送货别名', brandMode: 'any', brandName: '' }, { aliasName: '', brandMode: '', brandName: '' }, {}, { aliasName: '另一段别名', brandMode: 'required', brandName: '另一段品牌' } ]) { test(`segment metadata overrides the same root product, including explicit empty values: ${JSON.stringify(metadata)}`, () => { const format = productLine() const root = { id: 'root', productId: 'p', productName: '标准名称', aliasName: '装货根别名', brandMode: 'required', brandName: '装货根品牌' } const plan = { products: [root] } const load = format(plan, { kind: 'load' }, { productId: 'p', planProductId: 'root' }) const delivery = format( plan, { kind: 'delivery' }, { productId: 'p', planProductId: 'root', mobileProductMeta: metadata } ) assert.equal(load.alias, '装货根别名') assert.equal(load.brand, '装货根品牌') assert.equal(delivery.alias, metadata.aliasName || '—') assert.equal(delivery.brand, formatBrandConstraintLabel(metadata.brandMode, metadata.brandName)) assert.equal(root.aliasName, '装货根别名') assert.equal(root.brandName, '装货根品牌') }) } function queryFunction(path: string, name: string, bindings: Record) { 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( (node) => ts.isFunctionDeclaration(node) && node.name?.text === name )! return new Function( ...Object.keys(bindings), `${ts.transpile(fn.getText(ast), { target: ts.ScriptTarget.ESNext })};return ${name}` )(...Object.values(bindings)) } for (const tab of ['pending_arrange', 'all', 'inbound', 'outbound']) { test(`mobile ${tab} list query matches the Web keyword, filter, tab, sort and contract-side parameters`, () => { const state = useServerColumnFilters(plPlanColumnFilterDefs) Object.assign(state.filters, { followStatus: { selected: ['accepted'], text: '' }, planType: { selected: ['direct'], text: '' }, planNo: { selected: ['PO1', 'PO2'], text: '' }, planDate: { from: '2026-10-01', to: '2026-10-10' }, deliveryTimes: { from: '2026-10-02', to: '2026-10-11' }, loadCustomer: { selected: ['供方'], text: '' }, deliveryCustomer: { selected: ['需方'], text: '' }, productContract: { selected: ['load', 'CT1'], text: '' }, personnelSummary: { text: '司机', selected: [] }, planQty: { op: 'between', min: 1, max: 9 } }) const keyword = ref(' 搜索关键词 '), activeTab = ref(tab), dateRange = ref(['2026-09-01', '2026-09-30']) const sort = { sortField: 'deliveryTimes', sortOrder: 'asc' } const bindings = { ...state, keyword, activeTab, dateRange, sort, pageSize: 20, plPlanListTabToFilters, applyMergedCustomerQueryParams, applyContractSideQueryParams, isStockList: ref(false), quickSearch: keyword, activePlanTypeTab: activeTab, toSortQueryParams: () => sort } const mobile = queryFunction('../src/views/m/pl/plan/index.vue', 'buildParams', bindings)() const web = queryFunction('../src/views/pl/plan/index.vue', 'buildListQueryParams', bindings)() assert.deepEqual(mobile, { ...web, pageSize: 20 }) }) } test('plan facets preserve independent load and delivery customer restrictions from the draft', () => { const state = useServerColumnFilters(plPlanColumnFilterDefs) Object.assign(state.filters, { loadCustomer: { text: '', selected: ['装货甲'] }, deliveryCustomer: { text: '', selected: ['送货乙'] } }) const source = ts.createSourceFile( 'filter.ts', parse(readFileSync(new URL('../src/views/m/pl/plan/PlanFilters.vue', import.meta.url), 'utf8')) .descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const node = source.statements.find( (node) => ts.isVariableStatement(node) && node.declarationList.declarations.some((d) => d.name.getText(source) === 'picker') )! const env = { defs: plPlanColumnFilterDefs, draft: state.filters, toQueryParams: state.toQueryParams, props: { context: { followStatus: 'accepted', page: 2, loadCustomersIn: '旧装货', deliveryCustomersIn: '旧送货' }, filters: {} }, userStore: {}, useMobileCertFacet: (config: any) => config, fetchPlanColumnFacetValues: () => {}, applyMergedCustomerQueryParams, applyContractSideQueryParams, facetPlanDateParams } const picker = new Function( ...Object.keys(env), `${ts.transpile(node.getText(source), { target: ts.ScriptTarget.ESNext })};return picker` )(...Object.values(env)) const q = picker.context() assert.equal(q.loadCustomersIn, '装货甲') assert.equal(q.deliveryCustomersIn, '送货乙') assert.equal(q.stopCustomers, undefined) assert.equal(q.followStatus, 'accepted') }) import * as Vue from 'vue' import { useMobileCertFacet } from '../src/views/m/pl/plan-cert/useMobileCertFacet' import { withPlanContractIoFacetOptions, PL_PLAN_CONTRACT_SIDE_FILTER_OPTIONS } from '../src/utils/pl-plan-contract-display' test('plan filter pickers confirm drafts, keep server query semantics and reset all filters', async () => { const source = ts.createSourceFile( 'filters.ts', parse(readFileSync(new URL('../src/views/m/pl/plan/PlanFilters.vue', import.meta.url), 'utf8')) .descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const code = source.statements .filter((node) => !ts.isImportDeclaration(node)) .map((node) => node.getText(source)) .join('\n') const props = Vue.reactive({ modelValue: false, dateOnly: false, filters: {} as Record, sort: { sortField: '', sortOrder: 'desc' }, context: { followStatus: 'accepted' } }) const events: any[] = [], requests: any[] = [], warnings: string[] = [] const bindings = { ...Object.fromEntries( Object.entries(Vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default') ), defs: plPlanColumnFilterDefs, useServerColumnFilters, useMobileCertFacet, applyMergedCustomerQueryParams, applyContractSideQueryParams, facetPlanDateParams, withPlanContractIoFacetOptions, PL_PLAN_CONTRACT_SIDE_FILTER_OPTIONS, defineProps: () => props, defineEmits: () => (...args: any[]) => events.push(args), useUserStore: () => ({ accessToken: 'test', getUserInfo: { id: 'U' } }), fetchPlanColumnFacetValues: async (query: any) => { requests.push(query) return { list: [{ value: 'P1', label: '计划一' }] } }, ElMessage: { warning: (text: string) => warnings.push(text) } } const scope = Vue.effectScope() const h = scope.run(() => new Function( ...Object.keys(bindings), ts.transpile(code, { target: ts.ScriptTarget.ESNext }) + ';return {picker,draft,confirmPicker,enumSummary,openSingle,confirmSingle,setNumberValue,openDate,confirmDate,toQueryParams,apply,resetAndApply,singleOpen,dateOpen,visibleDateDefs}' )(...Object.values(bindings)) ) const flush = async () => { for (let i = 0; i < 10; i++) await Promise.resolve() } try { props.modelValue = true await Vue.nextTick() h.picker.open('planType', []) await flush() assert.equal(requests.length, 0, 'fixed options do not call the remote facet API') h.picker.toggle('inbound') h.picker.close() assert.equal(h.draft.planType, undefined) h.picker.open('planType', []) await flush() h.picker.toggle('inbound') h.confirmPicker() assert.equal(h.enumSummary('planType'), '入库单') h.picker.open('planNo', []) await flush() assert.equal(requests[0].followStatus, 'accepted') h.picker.toggle('P1') h.confirmPicker() assert.equal(h.enumSummary('planNo'), '计划一') h.openSingle('numberOp', 'planQty') h.confirmSingle({ selectedValues: ['gte'] }) h.setNumberValue('planQty', 'value', '10') h.openDate('planDate', 'from') h.confirmDate({ selectedValues: ['2026', '10', '24'] }) h.openSingle('sortField') h.confirmSingle({ selectedValues: ['planDate'] }) assert.deepEqual(h.toQueryParams(), { planNoIn: 'P1', planTypeIn: 'inbound', planDateFrom: '2026-10-24', productPlanQtyOp: 'gte', productPlanQty: '10' }) h.apply() assert.deepEqual(events.find((event) => event[0] === 'apply').slice(2), [ { sortField: 'planDate', sortOrder: 'desc' }, false ]) h.openDate('planDate', 'to') h.confirmDate({ selectedValues: ['2026', '10', '23'] }) h.apply() assert.equal(warnings.length, 1) h.openSingle('sortOrder') h.openDate('planDate', 'to') props.modelValue = false await Vue.nextTick() assert.equal(h.singleOpen.value, false) assert.equal(h.dateOpen.value, false) h.resetAndApply() assert.deepEqual(events.filter((event) => event[0] === 'apply').at(-1), [ 'apply', {}, { sortField: '', sortOrder: 'desc' }, true ]) props.modelValue = false await Vue.nextTick() props.dateOnly = true props.filters = { planNo: { selected: ['P1'] }, planDate: { from: '2026-10-24', to: '2026-10-25' } } props.sort = { sortField: 'createTime', sortOrder: 'asc' } props.modelValue = true await Vue.nextTick() assert.deepEqual( h.visibleDateDefs.value.map((d: any) => d.prop), ['planDate'] ) h.openDate('planDate', 'from') h.confirmDate({ selectedValues: ['2026', '10', '23'] }) assert.equal( props.filters.planDate.from, '2026-10-24', 'date edits stay in draft until confirmed' ) h.resetAndApply() assert.deepEqual(events.filter((event) => event[0] === 'apply').at(-1), [ 'apply', { planNo: { selected: ['P1'] } }, { sortField: 'createTime', sortOrder: 'asc' }, true ]) } finally { scope.stop() } }) test('date entry opens only date fields and preserves applied quick dates until confirmed', () => { const source = ts.createSourceFile( 'list.ts', parse(readFileSync(new URL('../src/views/m/pl/plan/index.vue', import.meta.url), 'utf8')) .descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true ) const names = [ 'rangeFilters', 'onRangeOpen', 'onMoreOpen', 'onDateDropdownChange', 'pickDate', 'applyFilters' ] const code = source.statements .filter((node) => ts.isFunctionDeclaration(node) ? names.includes(node.name!.text) : ts.isVariableStatement(node) && node.declarationList.declarations.some((d) => names.includes(d.name.getText(source))) ) .map((node) => node.getText(source)) .join('\n') let reloads = 0 const bindings = { computed: Vue.computed, filters: Vue.reactive>({ planNo: { selected: ['P1'] } }), dateShortcut: ref('today'), dateRange: ref(['2026-10-24', '2026-10-24']), customFrom: ref(''), customTo: ref(''), customDateLabel: ref(''), dateOnly: ref(false), filterOpen: ref(false), rangeDropdown: ref(null), moreDropdown: ref(null), sort: { sortField: 'createTime', sortOrder: 'asc' }, reload: () => reloads++, closeFilter: () => {}, rangeFor: () => ['2026-10-25', '2026-10-25'] } const h = new Function( ...Object.keys(bindings), ts.transpile(code, { target: ts.ScriptTarget.ESNext }) + ';return {rangeFilters,onRangeOpen,onMoreOpen,onDateDropdownChange,pickDate,applyFilters}' )(...Object.values(bindings)) h.onRangeOpen() assert.equal(bindings.dateOnly.value, true) assert.equal(bindings.filterOpen.value, true) assert.equal(bindings.dateShortcut.value, 'today') assert.equal(bindings.filters.planDate, undefined) assert.deepEqual(h.rangeFilters.value.planDate, { from: '2026-10-24', to: '2026-10-24' }) bindings.filterOpen.value = false h.onDateDropdownChange('custom') assert.equal(bindings.dateShortcut.value, 'today') assert.equal(reloads, 0) h.applyFilters( { ...bindings.filters, planDate: { from: '2026-10-20', to: '2026-10-22' } }, { ...bindings.sort } ) assert.equal(bindings.dateShortcut.value, 'custom') assert.deepEqual(bindings.dateRange.value, []) assert.equal(bindings.customDateLabel.value, '2026-10-20 ~ 2026-10-22') h.onMoreOpen() assert.equal(bindings.dateOnly.value, false) h.pickDate('custom') assert.equal(bindings.filters.planDate.from, '2026-10-20') h.pickDate('tomorrow') assert.equal(bindings.filters.planDate, undefined) assert.deepEqual(bindings.dateRange.value, ['2026-10-25', '2026-10-25']) assert.deepEqual(bindings.filters.planNo, { selected: ['P1'] }) })