| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- import assert from 'node:assert/strict'
- import { readFileSync, existsSync } from 'node:fs'
- import test from 'node:test'
- import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
- import ts from 'typescript'
- const root = new URL('../src/', import.meta.url)
- const read = (path: string) => readFileSync(new URL(path, root), 'utf8')
- const components = ['pl-plan-load-section','pl-plan-delivery-section','pl-plan-stop-product-table','pl-plan-contract-multi-select','pl-address-with-library']
- for (const component of components) test(`${component}: mobile opt-in root and unchanged desktop template`, () => {
- const path = `components/business/${component}/index.vue`
- const d = parse(read(path)).descriptor
- const roots = d.template!.ast.children.filter((n: any) => n.type === 1) as any[]
- assert.match(roots[0].loc.source, /v-if="mobile/)
- const baseline = parse(readFileSync(`/tmp/mobile-r3-before/${path}`, 'utf8')).descriptor
- const old = (baseline.template!.ast.children.find((n: any) => n.type === 1) as any).loc.source
- assert.equal(roots[1].loc.source.replace(/\s+v-else(?=\s|>)/, ''), old)
- })
- for (const component of ['MobilePlanLoadSection','MobilePlanDeliverySection','MobilePlanProductCards','MobilePlanContractCards','PlanFormPickerSheets']) test(`${component}: mobile template compiles without desktop controls`, () => {
- const path = `views/m/pl/plan/${component}.vue`
- assert.ok(existsSync(new URL(path, root)), 'independent mobile component exists')
- const d = parse(read(path)).descriptor
- assert.doesNotMatch(d.template!.content, /<el-(?:input|select|table|button|date-picker)\b/)
- compileScript(d, { id: component })
- assert.deepEqual(compileTemplate({source:d.template!.content, filename:path,id:component}).errors, [])
- })
- test('actual scroll controller accepts fractional layout position within one pixel', () => {
- const source = parse(read('views/m/pl/plan/MobilePlPlanForm.vue')).descriptor.scriptSetup!.content
- const ast = ts.createSourceFile('scroll.ts', source, ts.ScriptTarget.Latest, true)
- const fn = ast.statements.find(n => ts.isFunctionDeclaration(n) && n.name?.text === 'syncScroll')!
- const active = { value: 'load' }
- const run = new Function('syncViewport','clickLock','navRef','sections','window','document','active',ts.transpile(fn.getText(ast))+';return syncScroll')
- run(()=>{},0,{value:{getBoundingClientRect:()=>({bottom:111})}},[{id:'load'},{id:'delivery'}],{scrollY:2459,innerHeight:800},{documentElement:{scrollHeight:6000},getElementById:(id:string)=>({getBoundingClientRect:()=>({top:id.endsWith('delivery')?123.46875:-100})})},active)()
- assert.equal(active.value,'delivery')
- })
- function productionFunction(path: string, name: string, bindings: Record<string, any>) {
- const ast = ts.createSourceFile('controller.ts', parse(read(path)).descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true)
- const fn = ast.statements.find(n => ts.isFunctionDeclaration(n) && n.name?.text === name)
- assert.ok(fn, `production function ${name}`)
- return new Function(...Object.keys(bindings), ts.transpile(fn.getText(ast)) + `;return ${name}`)(...Object.values(bindings))
- }
- test('mobile delivery preference response follows the original segment through reorder', async () => {
- let resolve: (v: any) => void = () => {}
- const a = { customerId: 'same', products: [], address: '' }, b = { customerId: 'same', products: [], address: 'B' }
- const props = { mobileSheet: true, modelValue: [a, b] }
- const calls: any[] = []
- const fn = productionFunction('components/business/pl-plan-delivery-section/index.vue', 'applyDeliveryStopPref', {
- props, customerStopPrefApi: { get: () => new Promise(r => { resolve = r }) }, pickBxData: (x: any) => x,
- patchDelivery: (i: number, value: any) => calls.push([i,value]),
- mobileStopIdentity: (x: any) => x.products
- })
- const pending = fn(0, 'same'); props.modelValue = [b, a]
- resolve({ address: 'A' }); await pending
- assert.deepEqual(calls, [[1,{address:'A'}]])
- })
- test('last mobile delivery cannot be removed by exposed controller', () => {
- const row = { customerId:'A' }, calls: any[]=[]
- const fn = productionFunction('components/business/pl-plan-delivery-section/index.vue','removeDelivery',{props:{mobileSheet:true,modelValue:[row]},emit:(...a:any[])=>calls.push(a),resequence:(a:any)=>a,createEmptyDeliveryForm:()=>({}),activeIndex:{value:0}})
- fn(0); assert.deepEqual(calls,[])
- })
- test('quantity input retains decimal intermediate text while committing three decimals on blur', () => {
- const quantityDrafts = new Map<any,string>(), row:any={planQty:0}
- const path='views/m/pl/plan/MobilePlanProductCards.vue'
- const set=productionFunction(path,'setQuantity',{quantityDrafts,c:{props:{editable:true}}})
- for(const input of ['1','1.','1.1','1.12','1.125']) { set(row,input); assert.equal(quantityDrafts.get(row),input) }
- assert.equal(row.planQty,1.125)
- })
- test('mobile metadata persistence blocks save until authoritative refresh and ignores removed rows', async () => {
- const path = new URL('../src/views/m/pl/plan/mobilePlanMetadata.ts', import.meta.url)
- assert.ok(existsSync(path),'mobile metadata guard exists')
- const {createMobilePlanMetadata} = await import(path.href)
- const a:any={productId:'p',aliasOptions:['old']}, b:any={productId:'p',aliasOptions:['old']}
- let rows=[a,b]
- const guard=createMobilePlanMetadata(()=>rows)
- guard.persisted(a); assert.equal(guard.ready(),false)
- guard.saved(a,{aliases:[{aliasName:'fresh'}],brands:[]})
- assert.equal(guard.ready(),true); assert.deepEqual(b.aliasOptions,['fresh'])
- rows=[]; guard.persisted(a); assert.equal(guard.ready(),true)
- })
- test('obsolete mobile contract detail response cannot replace a newer customer snapshot', async () => {
- let resolve:(v:any)=>void=()=>{}
- const props:any={mobile:true,customerId:'A',modelValue:[{contractId:'c1'}]}, detailRows:any={value:[]}, loadingDetails={value:false}
- const fn=productionFunction('components/business/pl-plan-contract-multi-select/index.vue','loadDetails',{props,detailRows,loadingDetails,getContractDetailCached:()=>new Promise(r=>resolve=r),enrichContractProducts:async(x:any)=>x,mobileDetailVersion:{value:0}})
- const pending=fn(['c1']); props.customerId='B'; detailRows.value=[{id:'c2'}]
- resolve({id:'c1',products:[]});await pending
- assert.deepEqual(detailRows.value,[{id:'c2'}])
- })
- import { buildLoadPayload, buildDeliveriesPayload, createEmptyLoadForm, createEmptyDeliveryForm, resolveStopDibangDefaults, canManualToggleStopDibang } from '../src/utils/pl-plan-stop-form'
- import { collectStopContractIds, unionContractProductRows, allowedPackageSpecsForStopProduct, normalizePackageSpec } from '../src/utils/pl-plan-contract-product'
- import { isPlInternalCustomerName } from '../src/utils/pl-internal-customer'
- import { findFirstStopProductArrangeGap } from '../src/utils/pl-plan-stop-product-arrange-required'
- import { mobileStopIdentity } from '../src/views/m/pl/plan/mobileStopIdentity'
- test('payload keeps two same-product contract rows and original identities; mobile token never serializes', () => {
- const load:any={...createEmptyLoadForm(),id:'stop',products:[{productId:'p',planProductId:'pp1',contractId:'a',contractProductId:'ca',packageSpec:'桶',planQty:1.125},{productId:'p',planProductId:'pp2',contractId:'b',contractProductId:'cb',packageSpec:'袋',planQty:2}]}
- const identity=mobileStopIdentity(load)
- assert.equal(mobileStopIdentity({...load}),identity)
- const payload=buildLoadPayload(load)
- assert.equal(payload.id,'stop');assert.equal(payload.products.length,2)
- assert.deepEqual(payload.products.map(p=>[p.planProductId,p.contractId,p.contractProductId,p.packageSpec]),[['pp1','a','ca','桶'],['pp2','b','cb','袋']])
- assert.equal(Object.getOwnPropertySymbols(payload).length,0)
- const a:any={...createEmptyDeliveryForm(),id:'delivery',contact:'19900001111',notes:'保留',products:load.products}
- mobileStopIdentity(a)
- assert.equal(buildDeliveriesPayload([a])[0].contact,'19900001111')
- assert.equal(Object.getOwnPropertySymbols(buildDeliveriesPayload([a])[0]).length,0)
- })
- test('actual formal contract validator rejects external no-contract and permits exact internal names', () => {
- const loadForm={value:{customerName:'外部',contracts:[]}}, deliveryForms={value:[]}, messages:string[]=[]
- const bindings={loadForm,deliveryForms,isPlInternalCustomerName,ElMessage:{warning:(s:string)=>messages.push(s)}}
- const stopHasSelectedContract=productionFunction('components/business/pl-plan-form-dialog/index.vue','stopHasSelectedContract',{})
- const valid=productionFunction('components/business/pl-plan-form-dialog/index.vue','validateStopContractsRequired',{...bindings,stopHasSelectedContract})
- assert.equal(valid(),false)
- loadForm.value.customerName='安庆市山峡化工有限公司';assert.equal(valid(),true)
- loadForm.value.customerName='安徽云栋化工有限公司';assert.equal(valid(),true)
- loadForm.value.customerName='安庆市山峡化工有限公司分公司';assert.equal(valid(),false)
- })
- test('actual formal packaging validator distinguishes same product contract line packaging', () => {
- const product:any={productId:'p',contractId:'a',contractProductId:'ca',productName:'产品',packageSpec:'袋'}
- const loadForm={value:{contracts:[{contractId:'a'},{contractId:'b'}],products:[product]}}
- const contractProductRowsCache={value:{a:[{id:'ca',productId:'p',contractId:'a',packageSpec:'桶'}],b:[{id:'cb',productId:'p',contractId:'b',packageSpec:'袋'}]}}
- const valid=productionFunction('components/business/pl-plan-form-dialog/index.vue','validateContractPackageSpecs',{loadForm,deliveryForms:{value:[]},contractProductRowsCache,collectStopContractIds,unionContractProductRows,allowedPackageSpecsForStopProduct,normalizePackageSpec,ElMessage:{error:()=>{}}})
- assert.equal(valid(),false);product.packageSpec='桶';assert.equal(valid(),true)
- })
- test('zero quantity remains serializable for drafts but actual arrange validation rejects it', () => {
- const row:any={productId:'p',unit:'吨',packageSpec:'桶',deliveryMethod:'送货',planQty:0}
- assert.equal(buildLoadPayload({...createEmptyLoadForm(),products:[row]}).products[0].planQty,0)
- assert.match(findFirstStopProductArrangeGap({loadProducts:[row]})!,/计划数量/)
- row.planQty=1.125;assert.equal(findFirstStopProductArrangeGap({loadProducts:[row]}),null)
- })
- test('existing dibang controller rules preserve external prohibition and internal manual exception', () => {
- assert.deepEqual(resolveStopDibangDefaults('外部','load'),{generateDibang:0,dibangDirection:'none'})
- assert.deepEqual(resolveStopDibangDefaults('安庆市山峡化工有限公司','load'),{generateDibang:1,dibangDirection:'outbound'})
- assert.equal(canManualToggleStopDibang('外部','皖H21522'),false)
- assert.equal(canManualToggleStopDibang('安庆市山峡化工有限公司','皖H21522'),true)
- })
- test('actual internal catalog callback cannot append into a replaced mobile load segment', async () => {
- const loadForm:any={value:{products:[{productId:'old'}]}}, productSelectorTarget={value:{side:'load',deliveryIndex:0}}, isPageVariant={value:true}
- const fn=productionFunction('components/business/pl-plan-form-dialog/index.vue','handleProductConfirm',{loadForm,productSelectorTarget,isPageVariant,deliveryForms:{value:[]},pageLoadGeneration:1,isPageSubmitCurrent:()=>true,freightCalc:{resetFreightCalc:()=>{}},mobileStopIdentity})
- const pending=fn([]);loadForm.value={products:[{productId:'new'}]};await pending
- assert.deepEqual(loadForm.value.products,[{productId:'new'}])
- })
- test('mobile arrange quantity retry invalidates failed cache before existing loader', async () => {
- const cache={value:{key:null}}, calls:string[]=[]
- const fn=productionFunction('components/business/pl-plan-stop-product-table/index.vue','retryMobileArrangeQty',{arrangeQtyCache:cache,cacheKey:()=> 'key',invalidateArrangeQtyCache:(k:string)=>calls.push(k),ensureArrangeQty:async()=>({availableQty:3})})
- assert.deepEqual(await fn({}),{availableQty:3});assert.deepEqual(calls,['key']);assert.equal('key' in cache.value,false)
- })
- test('all unrelated existing child controller functions remain byte-identical to R3 baseline', () => {
- const allowed:Record<string,string[]>={
- 'pl-plan-delivery-section':['removeDelivery','applyDeliveryStopPref'],
- 'pl-plan-contract-multi-select':['toggleExpand','enrichContractProducts','loadDetails','handleConfirmSingle']
- }
- for(const name of components){
- const path=`components/business/${name}/index.vue`
- const parseAst=(s:string)=>ts.createSourceFile('controller.ts',parse(s).descriptor.scriptSetup!.content,ts.ScriptTarget.Latest,true)
- const before=parseAst(readFileSync(`/tmp/mobile-r3-before/${path}`,'utf8')), after=parseAst(read(path))
- for(const node of before.statements){
- if(!ts.isFunctionDeclaration(node)||!node.name)continue
- if(allowed[name]?.includes(node.name.text))continue
- const next=after.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name?.text===node.name!.text)
- assert.ok(next,`${name}.${node.name.text}`)
- assert.equal(next.getText(after),node.getText(before),`${name}.${node.name.text}`)
- }
- }
- })
- test('teleported plan picker explicitly owns primary button tokens',()=>{
- const styles=parse(read('views/m/pl/plan/PlanFormPickerSheets.vue')).descriptor.styles.map(s=>s.content).join('\n')
- assert.match(styles,/--van-button-primary-background:\s*#0a9f6d/)
- assert.match(styles,/--van-button-primary-border-color:\s*#0a9f6d/)
- })
- for(const product of [null,{}, {id:'wrong',aliases:[],brands:[]},{id:'p'}]) test(`metadata refresh rejects incomplete or wrong-product response ${JSON.stringify(product)}`,async()=>{
- const row={productId:'p'}, calls:any[]=[]
- const fn=productionFunction('views/m/pl/plan/MobilePlanProductCards.vue','refreshMetadata',{c:{props:{products:[row]}},fetchProductDetail:async()=>({data:product}),metadata:{saved:(...args:any[])=>calls.push(args),capture:()=>()=>true},metadataRefreshVersion:0})
- await fn(row);assert.equal(calls.length,0)
- })
|