2026-10-10 02:19:39 +08:00
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { runInNewContext } from 'node:vm'
import ts from 'typescript'
import * as vue from 'vue'
import { parse , compileScript } from 'vue/compiler-sfc'
let nextKey = 0
const crypto = { randomUUID : () => `test-key- ${ ++ nextKey } ` }
function moduleFrom ( source , require = () => { throw new Error ( 'unexpected import' ) }) {
const context = { exports : {}, require , URL , Error , crypto }
const code = ts . transpileModule ( source , { compilerOptions : { target : ts . ScriptTarget . ES2022 , module : ts . ModuleKind . CommonJS } }). outputText
runInNewContext ( code , context )
return context . exports
}
const presentation = moduleFrom ( readFileSync ( new URL ( './procurementPresentation.ts' , import . meta . url ), 'utf8' ))
const submission = moduleFrom ( readFileSync ( new URL ( './procurementSubmission.ts' , import . meta . url ), 'utf8' ), name => {
if ( name === './procurementPresentation' ) return presentation
throw new Error ( `unexpected import ${ name } ` )
})
const normalized = value => JSON . parse ( JSON . stringify ( value ))
const draft = () => ({ ... presentation . emptyProcurementDraft (), purchaseCategory : 'Sample' , productName : 'Test product' , productSpecification : 'A' ,
purchaseQuantity : '3' , budgetUnitPrice : '0.29' , receivingAddress : '测试省测试市测试街 1 号' , contactName : '测试收件人' , contactPhone : '13800000000' })
const fee = ( purpose , walletStatus , amount ) => ({ purpose , walletStatus , amount , currency : 'CNY' })
test ( 'budget distinguishes unknown from zero, preserves cents, and requires a real telephone/address' , () => {
assert . equal ( presentation . draftBudgetTotal ( draft ()), '0.87' )
const valid = presentation . buildProcurementCreate ( draft (), 'Authenticated Company' , 'same-key' )
assert . equal ( valid . customerName , 'Authenticated Company' )
assert . equal ( valid . budgetUnitPrice , 0.29 )
for ( const value of [ '' , '-1' , '1.001' , '90071992547409.91' ]) assert . throws (() => presentation . buildProcurementCreate ({ ... draft (), budgetUnitPrice : value }, 'Company' , 'key' ), /预算/ )
assert . equal ( presentation . buildProcurementCreate ({ ... draft (), budgetUnitPrice : '0' }, 'Company' , 'key' ). budgetUnitPrice , 0 )
for ( const patch of [{ contactPhone : 'wechat-name' }, { contactPhone : 'a@test.invalid' }, { receivingAddress : '' }, { purchaseQuantity : '1.5' }]) {
assert . throws (() => presentation . buildProcurementCreate ({ ... draft (), ... patch }, 'Company' , 'key' ))
}
assert . equal ( presentation . measurement ( 20 , 2 ), '20.00' )
assert . equal ( presentation . measurement ( '5.125' , 3 ), '5.125' )
assert . equal ( presentation . measurement ( '1.234' , 2 ), '待核对' )
assert . equal ( presentation . money ( null ), '—' )
assert . equal ( presentation . money ( 0 ), 'CNY 0.00' )
assert . equal ( presentation . productHref ( 'javascript:alert(1)' ), undefined )
assert . equal ( presentation . productHref ( 'https://example.com/item' ), 'https://example.com/item' )
})
test ( 'successful net debit excludes the original estimate and counts only successful refunds and differences' , () => {
const result = presentation . feeTotals ([ fee ( 'INTERNATIONAL_ESTIMATE' , 'SETTLED' , '100' ), fee ( 'INTERNATIONAL_FREIGHT' , 'SETTLED' , '100' ),
fee ( 'ADJUSTMENT' , 'SETTLED' , '20' ), fee ( 'ADJUSTMENT' , 'SETTLED' , '10' ), fee ( 'REFUND' , 'REFUNDED' , '20' ), fee ( 'REFUND' , 'REFUND_PENDING' , '5' )])
assert . deepEqual ( normalized ( result ), { paid : '130.00' , refunded : '20.00' , net : '110.00' , estimatedHold : '0.00' })
assert . equal ( presentation . feeTotals ([ fee ( 'INTERNATIONAL_ESTIMATE' , 'RELEASE_PENDING' , '100' )]). estimatedHold , '100.00' )
assert . equal ( presentation . feeTotals ([ fee ( 'DUTY' , 'SETTLED' , 'unknown' )]), null )
assert . equal ( presentation . feeStatus ( fee ( 'INTERNATIONAL_ESTIMATE' , 'FROZEN' , 100 )), '已冻结,未扣款' )
assert . equal ( presentation . feeStatus ( fee ( 'INTERNATIONAL_ESTIMATE' , 'SETTLED' , 100 )), '原预估已结清' )
assert . equal ( presentation . feeStatus ( fee ( 'DUTY' , null , 100 )), '待核定' )
})
2026-10-22 23:06:02 +08:00
test ( 'new requests get unique keys without restoring stored submissions' , () => {
assert . notEqual ( submission . procurementRequestKey ( crypto ), submission . procurementRequestKey ( crypto ))
2026-10-10 02:19:39 +08:00
})
class ApiError extends Error { constructor ( message , code , status ) { super ( message ); this . code = code ; this . status = status } }
const flush = async () => { await vue . nextTick (); await new Promise ( resolve => setImmediate ( resolve )); await vue . nextTick () }
function viewHarness ({ createPage = false , detailPage = false , api = {}, storage = new Map () } = {}) {
const props = vue . reactive ({ createPage , detailPage })
const route = vue . reactive ({ params : { orderNo : detailPage ? 'PO-1' : '' } })
const auth = vue . reactive ({ token : 'identity-A' , user : { tenantName : 'Company' }, profile : { tenantName : 'Company' }, hasPermission : () => true })
const pushes = [], dispose = []
const defaults = { listProcurements : async () => ({ records : [], total : 0 }), getProcurement : async orderNo => ({ order : { orderNo } }), createProcurement : async () => ({ order : { orderNo : 'PO-CREATED' } }) }
const { descriptor } = parse ( readFileSync ( new URL ( './views/ProcurementView.vue' , import . meta . url ), 'utf8' ))
const source = compileScript ( descriptor , { id : 'procurement-test' }). content
const context = { exports : {}, Error , crypto , sessionStorage : { getItem : key => storage . get ( key ) || null , setItem : ( key , value ) => storage . set ( key , value ), removeItem : key => storage . delete ( key ) },
require ( name ) {
if ( name === 'vue' ) return { ... vue , onBeforeUnmount : callback => dispose . push ( callback ), watch : (... args ) => { const stop = vue . watch (... args ); dispose . push ( stop ); return stop } }
if ( name === 'vue-router' ) return { useRoute : () => route , useRouter : () => ({ push : async target => pushes . push ( target ) }) }
if ( name === 'element-plus' ) return { ElMessage : { success : () => {} } }
if ( name === '@/api/types' ) return { ApiError }
if ( name === '@/api/session' ) return { accessTokenIdentity : token => token }
if ( name === '@/modules/auth/public' ) return { useAuthStore : () => auth }
if ( name === '../api/procurement' ) return { ... defaults , ... api }
if ( name === '../procurementPresentation' ) return presentation
if ( name === '../procurementSubmission' ) return submission
if ( name . endsWith ( '.vue' )) return { default : {} }
throw new Error ( `unexpected view import ${ name } ` )
},
}
runInNewContext ( ts . transpileModule ( source , { compilerOptions : { target : ts . ScriptTarget . ES2022 , module : ts . ModuleKind . CommonJS } }). outputText , context )
const state = context . exports . default . setup ( props , { expose : () => {} })
return { state , props , route , auth , pushes , storage , dispose : () => dispose . forEach ( callback => callback ()) }
}
test ( 'detail has one in-flight request and an older response cannot overwrite another order' , async () => {
const calls = [], pending = new Map ()
const harness = viewHarness ({ detailPage : true , api : { getProcurement : orderNo => { calls . push ( orderNo ); return new Promise ( resolve => pending . set ( orderNo , resolve )) } } })
try {
assert . deepEqual ( calls , [ 'PO-1' ])
await harness . state . loadDetail (); assert . equal ( calls . length , 1 )
harness . route . params . orderNo = 'PO-2' ; await flush (); assert . deepEqual ( calls , [ 'PO-1' , 'PO-2' ])
pending . get ( 'PO-1' )({ order : { orderNo : 'PO-1' } }); await flush (); assert . equal ( harness . state . detail . value , null )
pending . get ( 'PO-2' )({ order : { orderNo : 'PO-2' } }); await flush (); assert . equal ( harness . state . detail . value . order . orderNo , 'PO-2' )
assert . equal ( harness . state . loading . value , false )
} finally { harness . dispose () }
})
2026-10-22 23:06:02 +08:00
test ( 'timeout releases the form and another click submits the edited current values' , async () => {
const calls = []
const harness = viewHarness ({ createPage : true , api : { createProcurement : async payload => { calls . push ( normalized ( payload )); throw new ApiError ( 'timeout' , 'TIMEOUT' ) } } })
2026-10-10 02:19:39 +08:00
try {
2026-10-22 23:06:02 +08:00
Object . assign ( harness . state . form , draft ()); await harness . state . submit ()
assert . equal ( harness . state . submitting . value , false ); assert . equal ( harness . storage . size , 0 )
harness . state . form . productName = 'changed' ; await harness . state . submit ()
assert . equal ( calls [ 1 ]. productName , 'changed' ); assert . notEqual ( calls [ 0 ]. idempotencyKey , calls [ 1 ]. idempotencyKey )
assert . equal ( harness . state . submitting . value , false )
} finally { harness . dispose () }
2026-10-10 02:19:39 +08:00
})
test ( 'submission double-click sends once and definitive validation rejection permits correction' , async () => {
let reject , calls = 0
const harness = viewHarness ({ createPage : true , api : { createProcurement : () => { calls ++ ; return new Promise (( _ , failure ) => { reject = failure }) } } })
try {
Object . assign ( harness . state . form , draft ()); const first = harness . state . submit (); await harness . state . submit (); assert . equal ( calls , 1 )
reject ( new ApiError ( 'invalid' , 'A0422' )); await first
2026-10-22 23:06:02 +08:00
assert . equal ( harness . state . submitting . value , false ); assert . equal ( harness . storage . size , 0 )
2026-10-10 02:19:39 +08:00
} finally { harness . dispose () }
})
test ( 'pagination batches one request and switching account clears old rows before the new response' , async () => {
const calls = []; let resolveSecond
const harness = viewHarness ({ api : { listProcurements : async params => {
calls . push ( normalized ( params )); if ( calls . length < 3 ) return { records : [{ orderNo : 'PO-A' }], total : 100 }
return new Promise ( resolve => { resolveSecond = resolve })
} } })
try {
await flush (); assert . equal ( calls . length , 1 )
harness . state . query . page = 2 ; harness . state . query . size = 50 ; await flush (); assert . equal ( calls . length , 2 )
harness . auth . token = 'identity-B' ; await vue . nextTick (); assert . equal ( harness . state . rows . value . length , 0 ); assert . equal ( harness . state . total . value , 0 )
resolveSecond ({ records : [{ orderNo : 'PO-B' }], total : 1 }); await flush (); assert . equal ( harness . state . rows . value [ 0 ]. orderNo , 'PO-B' )
assert . ok ( calls . every ( params => ! ( 'customerId' in params ) && ! ( 'tenantId' in params )))
} finally { harness . dispose () }
})
2026-10-22 23:06:02 +08:00
test ( 'consecutive timeout and permission failures never persist or freeze a submission' , async () => {
let attempt = 0
const harness = viewHarness ({ createPage : true , api : { createProcurement : async () => { throw new ApiError ( 'failed' , ++ attempt === 1 ? 'TIMEOUT' : 'A0403' ) } } })
2026-10-10 02:19:39 +08:00
try {
2026-10-22 23:06:02 +08:00
Object . assign ( harness . state . form , draft ()); await harness . state . submit (); await harness . state . submit ()
assert . equal ( attempt , 2 ); assert . equal ( harness . state . submitting . value , false ); assert . equal ( harness . storage . size , 0 )
harness . state . form . productName = 'editable' ; assert . equal ( harness . state . form . productName , 'editable' )
2026-10-10 02:19:39 +08:00
} finally { harness . dispose () }
})
2026-10-20 11:54:44 +08:00
test ( 'purchase category is optional while its length remains bounded' , () => {
const payload = presentation . buildProcurementCreate ({ ... draft (), purchaseCategory : ' ' }, 'Company' , 'key' )
assert . equal ( payload . purchaseCategory , '' )
assert . throws (() => presentation . buildProcurementCreate ({ ... draft (), purchaseCategory : 'x' . repeat ( 129 ) }, 'Company' , 'key' ), /品类/ )
})
2026-10-26 13:43:20 +08:00
test ( 'domestic purchase creates an export order and displays the actual origin and destination regions' , () => {
const payload = presentation . buildProcurementCreate ({ ... draft (), procurementDirection : 'DOMESTIC_TO_OVERSEAS' , receivingAddress : 'US, CA, Los Angeles, test destination' }, 'Authenticated Company' , 'export-key' )
assert . equal ( payload . procurementDirection , 'DOMESTIC_TO_OVERSEAS' )
assert . equal ( payload . receivingAddress , 'US, CA, Los Angeles, test destination' )
assert . equal ( presentation . statusLabel ( 'WAIT_OVERSEAS_RECEIPT' , payload . procurementDirection ), '待国内到仓' )
assert . equal ( presentation . statusLabel ( 'DOMESTIC_RECEIVING' , payload . procurementDirection ), '海外收货中' )
assert . equal ( presentation . statusLabel ( 'WAIT_CHARGES' , payload . procurementDirection ), '待海外核费' )
assert . equal ( presentation . statusLabel ( 'WAIT_OVERSEAS_RECEIPT' ), '待海外到仓' )
assert . throws (() => presentation . buildProcurementCreate ({ ... draft (), procurementDirection : 'INVALID' }, 'Company' , 'key' ), /采购方向/ )
const source = readFileSync ( new URL ( './views/ProcurementView.vue' , import . meta . url ), 'utf8' )
assert . match ( source , /v-model="form.procurementDirection"/ )
assert . match ( source , /regionText\('最终国内收件资料', form.procurementDirection\)/ )
})