Files
shanxia2.0/web-admin/scripts/mobile-pl-plan-list-parity.test.ts

466 lines
16 KiB
TypeScript

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<string, any>) {
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<string, any>,
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<Record<string, any>>({ 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'] })
})