| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183 |
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import test from 'node:test'
- import { effectScope, nextTick, ref, watch } from 'vue'
- import { parse } from '@vue/compiler-sfc'
- import ts from 'typescript'
- import { preserveMobileCopyAssociations, assertMobilePersistenceCapability } from '../src/views/m/pl/plan/mobilePlanPersistence'
- import { mobileStopIdentity } from '../src/views/m/pl/plan/mobileStopIdentity'
- import * as brand from '../src/utils/crm-product-brand'
- import { collectStopContractIds, filterProductsByAllowedProductIds } from '../src/utils/pl-plan-contract-product'
- import {
- describeUntaggedExternalStopProducts,
- retainStopProductsForContracts
- } from '../src/utils/pl-plan-stop-form'
- const source = readFileSync(
- new URL('../src/components/business/pl-plan-form-dialog/index.vue', import.meta.url),
- 'utf8'
- )
- const descriptor = parse(source).descriptor
- const ast = ts.createSourceFile(
- 'form.ts',
- descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- function declaration(name: string) {
- return ast.statements
- .find(
- (node) =>
- (ts.isFunctionDeclaration(node) && node.name?.text === name) ||
- (ts.isVariableStatement(node) &&
- node.declarationList.declarations.some((d) => d.name.getText(ast) === name))
- )
- ?.getText(ast)
- }
- function deferred<T = any>() {
- let resolve!: (value: T) => void
- let reject!: (reason: any) => void
- const promise = new Promise<T>((yes, no) => {
- resolve = yes
- reject = no
- })
- return { promise, resolve, reject }
- }
- function harness(
- options: {
- page?: boolean
- copy?: boolean
- newPlan?: boolean
- get?: any
- master?: any
- contract?: any
- enrich?: any
- realDependencies?: boolean
- realArrange?: boolean
- overrides?: Record<string, any>
- } = {}
- ) {
- const writes: any[] = []
- const events: any[] = []
- const invalidations: string[] = []
- const noop = () => {}
- const bindings: Record<string, any> = {
- mobileStopIdentity,
- preserveMobileCopyAssociations,
- buildMobileProductLinks: () => ({}),
- assertMobilePersistenceCapability: () => {},
- ref,
- Error,
- String,
- Object,
- Promise,
- Set,
- Number,
- props: {
- planId: options.copy || options.newPlan ? undefined : 'plan-1',
- copyFromPlanId: options.copy ? 'plan-1' : undefined
- },
- isPageVariant: ref(options.page !== false),
- isEdit: ref(!options.copy && !options.newPlan),
- detailLoading: ref(false),
- submitLoading: ref(false),
- suppressAddressMutex: ref(false),
- suppressStopProductSync: ref(false),
- formData: { belongsCompany: 'shanxia', createDate: '2026-10-21', products: [] },
- loadForm: ref({ products: [], contracts: [] }),
- deliveryForms: ref([]),
- loadedPlanSnapshot: ref(null),
- loadedTransportStatus: ref('pending'),
- loadedFollowStatus: ref(''),
- selectedDriverId: ref(''),
- selectedEscortId: ref(''),
- vehicleBundle: ref(null),
- contractProductIdsCache: ref({}),
- contractProductRowsCache: ref({}),
- contractLinesCache: ref({}),
- loadContractProductIds: ref([]),
- vehicleList: ref([]),
- carrierList: ref([]),
- driverList: ref([]),
- escortList: ref([]),
- customerOptions: ref([]),
- customerOptionsLoading: ref(false),
- vehicleLoading: ref(false),
- footerMoreOpen: ref(false),
- planApi: {
- mobileProductCapability: async () => ({ mobileProductMetaVersion: 1 }),
- get:
- options.get ||
- (async () => ({
- data: { id: 'plan-1', belongsCompany: 'shanxia', remark: '原计划', products: [] }
- })),
- update: async (...args: any[]) => {
- writes.push(['update', ...args])
- return { id: 'plan-1' }
- },
- createFull: async (...args: any[]) => {
- writes.push(['create', ...args])
- return { id: 'new-plan' }
- }
- },
- invalidateContractDetailCache: (id: string) => invalidations.push(id),
- getContractDetailCached: options.contract || (async () => ({ id: 'c1', products: [] })),
- enrichPlanProductsHazardFromCrm: options.enrich || (async (rows: any) => rows),
- enrichPlanProductLinesAlias: async (rows: any) => rows,
- mapPlanProductToFormRow: (row: any) => ({ ...row }),
- mapPlanProductToCopyFormRow: (row: any) => ({ ...row, id: undefined }),
- collectStopContractIds,
- describeUntaggedExternalStopProducts,
- retainStopProductsForContracts,
- collectContractProductRows: (rows: any) => rows,
- collectContractProductIds: (rows: any[]) => rows.map((row) => row.productId),
- unionContractProductIds: (cache: any, ids: string[]) => ids.flatMap((id) => cache[id] || []),
- resetForm: () => {
- bindings.formData.remark = ''
- bindings.formData.products = []
- bindings.loadedPlanSnapshot.value = null
- },
- applyStopsFromPlan: (data: any) => {
- bindings.loadForm.value = data.load || { products: [], contracts: [] }
- bindings.deliveryForms.value = data.deliveries || []
- },
- syncCommittedPartyIds: noop,
- bootstrapStopsFromPlanProducts: noop,
- syncSelectedCustomerOptions: noop,
- todayYmd: () => '2026-10-21',
- formatDate: () => '',
- resolveCurrentUserId: () => 'user',
- resolveCurrentUserName: () => '用户',
- freightCalc: { resetFreightCalc: noop, freightAmount: ref(null) },
- ensureSelectedVehicleOption: async () => {},
- ensureSelectedDriverOption: async () => {},
- ensureSelectedEscortOption: async () => {},
- ensureSelectedCarrierOption: async () => {},
- handleVehicleChange: async () => {},
- restoreFreightFromTransport: async () => {},
- reapplyStopDibangByVehiclePlate: noop,
- nextTick: async () => {},
- mobileMetadata: { ready: () => true },
- ElMessage: { error: noop, warning: noop, success: noop },
- emit: (...args: any[]) => events.push(args),
- innerVisible: ref(true),
- basicFormRef: ref({ validate: async () => true, validateField: async () => {} }),
- mirrorPlanProductsFromLoad: noop,
- buildPlanCommonPayload: () => ({
- remark: bindings.formData.remark,
- products: bindings.formData.products
- }),
- pickBxData: (data: any) => data.data ?? data,
- validateBusinessRules: () => true,
- validateSingleContractPerStop: () => true,
- validateContractPackageSpecs: () => true,
- validateStopContractsRequired: () => true,
- getArrangePrerequisiteGaps: () => [],
- validateStopProductsForArrange: () => true,
- showPlanSaveWarnings: noop,
- performArrangeAfterSave: async (id: string) => {
- writes.push(['arrange', id])
- return true
- },
- loadMasterData: options.master || (async () => {}),
- vehicleApi: { list: async () => [], get: async (id: string) => ({ id }) },
- carrierApi: { list: async () => [], get: async (id: string) => ({ id }) },
- personnelApi: { list: async () => [], get: async (id: string) => ({ id }) },
- fetchCustomerOptions: async () => [],
- extractCustomerOptions: (data: any) => data,
- pickBxList: (data: any) => data.data ?? data,
- searchVehicles: async () => {},
- searchCarriers: async () => {},
- searchDrivers: async () => {},
- searchEscorts: async () => {},
- loadDefaultCustomerOptions: async () => {},
- loadVehicleViewBundle: async () => ({ detail: {}, personnelList: [] }),
- transportApi: { get: async () => ({}) },
- resolveDefaultDriver: noop,
- resolveDefaultEscort: noop,
- buildMissingHazardCertReadyMessage: () => '',
- ElMessageBox: { alert: async () => {} },
- createArrangePresenter: () => async () => {},
- planHasAnyDibangSelected: () => false,
- loadArrangeHistoricalFlags: async () => ({}),
- confirmBeforePlanArrange: async () => {},
- buildArrangePayloadFromPlanForm: () => ({}),
- submitPlanArrange: async (id: string) => {
- writes.push(['arrange', id])
- },
- getUnitLabel: () => '',
- ...options.overrides
- }
- const names = [
- 'mobileSavedPlanId',
- 'mobileCreateUncertain',
- 'pageReady',
- 'pageLoadError',
- 'pageLoadGeneration',
- 'pageDisposed',
- 'isPageLoadCurrent',
- 'assertPageLoadCurrent',
- 'failPageLoad',
- 'invalidatePageLoad',
- 'validateLoadedPagePlan',
- 'handleOpened',
- 'ensureMobilePersistenceCapability',
- 'loadPlanForEdit',
- 'loadPlanForCopy',
- 'fetchContractProductIds',
- 'prefetchStopContractProducts',
- 'handleSubmit',
- 'isPageSubmitCurrent',
- 'pageLoadErrorMessage'
- ]
- names.push('ensurePageSelectedOption')
- if (options.realDependencies) {
- names.push('loadMasterData', 'handleVehicleChange', 'restoreFreightFromTransport')
- for (const name of ['loadMasterData', 'handleVehicleChange', 'restoreFreightFromTransport'])
- delete bindings[name]
- }
- if (options.realArrange) {
- names.push('performArrangeAfterSave')
- delete bindings.performArrangeAfterSave
- }
- const present = names.filter((name) => declaration(name))
- const code = ts.transpile(present.map((name) => declaration(name)).join('\n'), {
- target: ts.ScriptTarget.ESNext
- })
- const result = new Function(
- ...Object.keys(bindings),
- code + '; return {' + present.join(',') + '}'
- )(...Object.values(bindings))
- return { ...bindings, ...result, writes, events, invalidations }
- }
- async function blocked(h: ReturnType<typeof harness>) {
- for (const mode of ['draft', 'save', 'saveAndArrange', 'rearrange']) await h.handleSubmit(mode)
- assert.deepEqual(h.writes, [], 'failed initialization must never write or arrange')
- }
- for (const status of [404, 500])
- test(`mobile edit GET ${status} stays blocked`, async () => {
- const h = harness({
- get: async () => {
- throw new Error(`HTTP ${status}`)
- }
- })
- await h.handleOpened().catch(() => {})
- await blocked(h)
- assert.equal(h.pageReady.value, false)
- assert.match(h.pageLoadError.value, /404|500/)
- assert.equal(h.detailLoading.value, false)
- })
- test('mobile copy GET failure remains visible and blocked', async () => {
- const h = harness({
- copy: true,
- get: async () => {
- throw new Error('copy failed')
- }
- })
- await h.handleOpened().catch(() => {})
- await blocked(h)
- assert.ok(h.pageLoadError.value)
- assert.equal(
- h.events.some((event) => event[0] === 'update:visible'),
- false
- )
- })
- for (const data of [{}, { id: 'different' }, null])
- test(`mobile rejects invalid plan ${JSON.stringify(data)}`, async () => {
- const h = harness({ get: async () => ({ data }) })
- await h.handleOpened().catch(() => {})
- await blocked(h)
- assert.ok(h.pageLoadError.value)
- })
- test('new mobile base dependency failure cannot create', async () => {
- const h = harness({
- newPlan: true,
- master: async () => {
- throw new Error('base failed')
- }
- })
- await h.handleOpened().catch(() => {})
- await blocked(h)
- })
- test('contract failure preserves historical products, and formal save waits for a contract line', async () => {
- let failed = true
- const h = harness({
- get: async () => ({
- id: 'plan-1',
- belongsCompany: 'shanxia',
- remark: '原计划',
- products: [{ productId: 'p1' }],
- load: { contracts: [{ contractId: 'c1' }], products: [{ productId: 'p1', planQty: 12 }] }
- }),
- contract: async () => {
- if (failed) throw new Error('contract failed')
- return { id: 'c1', products: [{ productId: 'p1' }] }
- }
- })
- await h.handleOpened().catch(() => {})
- await blocked(h)
- assert.deepEqual(h.contractProductIdsCache.value, {})
- assert.equal(h.loadForm.value.products[0].planQty, 12)
- failed = false
- await h.handleOpened()
- assert.equal(h.pageReady.value, true)
- await h.handleSubmit('save')
- assert.deepEqual(h.writes, [])
- h.loadForm.value.products[0].contractId = 'c1'
- h.loadForm.value.products[0].contractProductId = 'line-1'
- await h.handleSubmit('save')
- assert.deepEqual(h.writes, [
- ['update', 'plan-1', { remark: '原计划', products: [{ productId: 'p1' }], saveMode: 'formal' }]
- ])
- })
- for (const lateError of [false, true])
- test(`retry ignores old ${lateError ? 'error' : 'success'} and finally`, async () => {
- const old = deferred()
- const fresh = deferred()
- let count = 0
- const h = harness({ get: () => (++count === 1 ? old.promise : fresh.promise) })
- const first = h.handleOpened().catch(() => {})
- while (count < 1) await Promise.resolve()
- const second = h.handleOpened()
- while (count < 2) await Promise.resolve()
- if (lateError) old.reject(new Error('old error'))
- else old.resolve({ id: 'plan-1', remark: 'stale' })
- await first
- assert.equal(h.detailLoading.value, true)
- assert.equal(h.formData.remark, '')
- assert.equal(h.pageLoadError.value, '')
- fresh.resolve({ id: 'plan-1', remark: 'fresh', products: [] })
- await second
- assert.equal(h.formData.remark, 'fresh')
- assert.equal(h.pageReady.value, true)
- })
- test('unmount invalidates pending detail without late ref writes or loading events', async () => {
- const request = deferred()
- const h = harness({ get: () => request.promise })
- const work = h.handleOpened()
- await Promise.resolve()
- await Promise.resolve()
- h.invalidatePageLoad()
- const before = JSON.stringify(h.formData)
- request.resolve({ id: 'plan-1', remark: 'stale' })
- await work
- assert.equal(JSON.stringify(h.formData), before)
- assert.equal(h.pageReady.value, false)
- })
- test('desktop retains copy failure close, failed contract empty cache and draft save behavior', async () => {
- const h = harness({
- page: false,
- copy: true,
- get: async () => {
- throw new Error('404')
- },
- contract: async () => {
- throw new Error('offline')
- }
- })
- await h.handleOpened()
- assert.ok(h.events.some((event) => event[0] === 'update:visible' && event[1] === false))
- assert.deepEqual(await h.fetchContractProductIds('c1'), [])
- assert.deepEqual(h.contractProductIdsCache.value, { c1: [] })
- await h.handleSubmit('draft')
- assert.equal(h.writes[0][0], 'create')
- })
- test('page template gates form and write actions on readiness and exposes retry/return', () => {
- assert.match(descriptor.template!.content, /v-if="isPageVariant && pageLoadError"/)
- assert.match(descriptor.template!.content, /v-show="!isPageVariant \|\| pageReady"/)
- assert.match(descriptor.template!.content, /v-if="isPageVariant && pageReady"/)
- assert.match(descriptor.template!.content, /重新加载/)
- assert.match(source, /onBeforeUnmount\(invalidatePageLoad\)/)
- })
- for (const dependency of ['vehicleApi', 'carrierApi', 'personnelApi', 'fetchCustomerOptions']) {
- test(`real mobile base loader fails closed for ${dependency}`, async () => {
- const fail = async () => {
- throw new Error(`${dependency} failed`)
- }
- const overrides =
- dependency === 'fetchCustomerOptions'
- ? { fetchCustomerOptions: fail }
- : { [dependency]: { list: fail } }
- const h = harness({ newPlan: true, realDependencies: true, overrides })
- await h.handleOpened()
- await blocked(h)
- assert.match(h.pageLoadError.value, /failed/)
- })
- }
- test('late base dependency cannot replace new option lists', async () => {
- const old = deferred()
- let count = 0
- const h = harness({
- newPlan: true,
- realDependencies: true,
- overrides: {
- vehicleApi: { list: () => (++count === 1 ? old.promise : Promise.resolve([{ id: 'fresh' }])) }
- }
- })
- const first = h.handleOpened()
- while (count < 1) await Promise.resolve()
- await h.handleOpened()
- old.resolve([{ id: 'old' }])
- await first
- assert.deepEqual(h.vehicleList.value, [{ id: 'fresh' }])
- assert.equal(h.pageReady.value, true)
- })
- test('late product enrichment cannot overwrite retry products or release new suppression flags', async () => {
- const old = deferred()
- const fresh = deferred()
- let count = 0
- const h = harness({ enrich: () => (++count === 1 ? old.promise : fresh.promise) })
- const first = h.handleOpened()
- while (count < 1) await Promise.resolve()
- const second = h.handleOpened()
- while (count < 2) await Promise.resolve()
- old.resolve([{ productId: 'old' }])
- await first
- assert.equal(h.suppressStopProductSync.value, true)
- assert.deepEqual(h.formData.products, [])
- fresh.resolve([{ productId: 'fresh' }])
- await second
- assert.deepEqual(h.formData.products, [{ productId: 'fresh' }])
- })
- test('late vehicle bundle cannot overwrite retry refs or clear the new vehicle loading flag', async () => {
- const old = deferred()
- const fresh = deferred()
- let count = 0
- const h = harness({
- realDependencies: true,
- get: async () => ({ id: 'plan-1', vehicleId: 'v1' }),
- overrides: { loadVehicleViewBundle: () => (++count === 1 ? old.promise : fresh.promise) }
- })
- const first = h.handleOpened()
- while (count < 1) await Promise.resolve()
- const second = h.handleOpened()
- while (count < 2) await Promise.resolve()
- old.resolve({ detail: { carrierId: 'old' }, personnelList: [] })
- await first
- assert.equal(h.vehicleLoading.value, true)
- assert.equal(h.vehicleBundle.value, null)
- fresh.resolve({ detail: { carrierId: 'fresh' }, personnelList: [] })
- await second
- assert.equal(h.formData.carrierId, 'fresh')
- })
- test('late contract success cannot populate the current generation cache', async () => {
- const old = deferred()
- const h = harness({ contract: () => old.promise })
- const fetch = h.fetchContractProductIds('c1').catch(() => {})
- await h.handleOpened()
- old.resolve({ products: [{ productId: 'old' }] })
- await fetch
- assert.deepEqual(h.contractProductIdsCache.value, {})
- assert.equal(h.pageLoadError.value, '')
- })
- test('successful mobile copy clears source ids and transport people before creating', async () => {
- const original = {
- id: 'plan-1',
- belongsCompany: 'shanxia',
- vehicleId: 'v1',
- driverId: 'd1',
- escortId: 'e1',
- load: {
- id: 'load-1',
- contracts: [],
- products: [{ id: 'line-1', planProductId: 'product-row-1' }]
- },
- deliveries: [
- { id: 'stop-1', contracts: [], products: [{ id: 'line-2', planProductId: 'product-row-2' }] }
- ]
- }
- const h = harness({ copy: true, get: async () => structuredClone(original) })
- await h.handleOpened()
- assert.equal(h.pageReady.value, true)
- assert.equal(h.loadForm.value.id, undefined)
- assert.equal(h.loadForm.value.products[0].planProductId, undefined)
- assert.equal(h.deliveryForms.value[0].id, undefined)
- assert.equal(h.deliveryForms.value[0].products[0].id, undefined)
- assert.equal(h.formData.vehicleId, '')
- assert.equal(h.selectedDriverId.value, '')
- assert.equal(h.selectedEscortId.value, '')
- await h.handleSubmit('draft')
- assert.equal(h.writes[0][0], 'create')
- assert.equal(
- h.writes.some((row) => row[0] === 'update'),
- false
- )
- })
- test('draft keeps minimal fields and formal save still requires original validation', async () => {
- const h = harness({ newPlan: true })
- await h.handleOpened()
- h.basicFormRef.value.validate = async () => false
- await h.handleSubmit('save')
- assert.deepEqual(h.writes, [])
- await h.handleSubmit('draft')
- assert.equal(h.writes[0][0], 'create')
- })
- test('desktop edit GET error still propagates and clears the loading state', async () => {
- const h = harness({
- page: false,
- get: async () => {
- throw new Error('desktop failure')
- }
- })
- await assert.rejects(h.handleOpened(), /desktop failure/)
- assert.equal(h.detailLoading.value, false)
- assert.equal(h.pageLoadError.value, '')
- })
- test('empty dependency error still provides a visible retry state', async () => {
- const h = harness({
- master: async () => {
- throw new Error('')
- }
- })
- await h.handleOpened()
- assert.ok(h.pageLoadError.value)
- })
- test('new initialization resets vehicle loading left by an invalidated generation', async () => {
- const h = harness({ newPlan: true })
- h.vehicleLoading.value = true
- await h.handleOpened()
- assert.equal(h.vehicleLoading.value, false)
- })
- for (const kind of ['driver', 'transport'])
- test(`${kind} refill failure prevents saving mobile details`, async () => {
- const fail = async () => {
- throw new Error(`${kind} failed`)
- }
- const h = harness({
- realDependencies: true,
- get: async () => ({
- id: 'plan-1',
- driverId: kind === 'driver' ? 'd1' : '',
- transportId: kind === 'transport' ? 't1' : ''
- }),
- overrides:
- kind === 'driver'
- ? { personnelApi: { list: async () => [], get: fail } }
- : { transportApi: { get: fail } }
- })
- await h.handleOpened()
- await blocked(h)
- assert.match(h.pageLoadError.value, /failed/)
- })
- function deliveryWatcher(h: ReturnType<typeof harness>) {
- const watcher = ast.statements.find(
- (node) =>
- ts.isExpressionStatement(node) &&
- node.getText(ast).startsWith('watch(') &&
- node.getText(ast).includes('contractKey: collectStopContractIds')
- )
- assert.ok(watcher)
- let callback!: (...args: any[]) => Promise<void>
- const bindings = {
- mobileStopIdentity, ...h,
- pageLoadGeneration: 1,
- deliveryContractFetchSeqByIndex: {},
- watch: (_source: any, cb: any) => {
- callback = cb
- }
- }
- new Function(
- ...Object.keys(bindings),
- ts.transpile(watcher.getText(ast), { target: ts.ScriptTarget.ESNext })
- )(...Object.values(bindings))
- return callback
- }
- test('mobile removing a delivery contract still clears its products', async () => {
- const h = harness()
- await h.handleOpened()
- h.deliveryForms.value = [{ products: [{ productId: 'p1', contractId: 'c1' }], contracts: [] }]
- await deliveryWatcher(h)([{ contractKey: '' }], [{ contractKey: 'c1' }])
- assert.deepEqual(h.deliveryForms.value[0].products, [])
- })
- test('delivery contract metadata failure preserves historical products', async () => {
- const h = harness({
- contract: async () => {
- throw new Error('offline')
- }
- })
- await h.handleOpened()
- h.deliveryForms.value = [{ products: [{ productId: 'p1' }], contracts: [{ contractId: 'c1' }] }]
- await deliveryWatcher(h)([{ contractKey: 'c1' }], [{ contractKey: 'old' }])
- assert.deepEqual(h.deliveryForms.value[0].products, [{ productId: 'p1' }])
- assert.equal(h.pageReady.value, false)
- })
- function enrichmentHelper(file: string, name: string, fetchProductDetail: any) {
- const text = readFileSync(new URL(`../src/composables/${file}.ts`, import.meta.url), 'utf8')
- const source = ts.createSourceFile('helper.ts', text, ts.ScriptTarget.Latest, true)
- const declaration = source.statements.find(
- (node) => ts.isFunctionDeclaration(node) && node.name?.text === name
- )
- assert.ok(declaration)
- const code = ts.transpile(declaration.getText(source).replace(/^export\s+/, ''), {
- target: ts.ScriptTarget.ESNext
- })
- return new Function('fetchProductDetail', ...Object.keys(brand), code + `;return ${name}`)(
- fetchProductDetail,
- ...Object.values(brand)
- )
- }
- for (const [file, name] of [
- ['usePlContractToPlan', 'enrichPlanProductLinesAlias'],
- ['usePlToxicProductCertCheck', 'enrichPlanProductsHazardFromCrm']
- ]) {
- test(`${name} strict mode rejects failed product metadata while default preserves existing behavior`, async () => {
- const helper = enrichmentHelper(file, name, async () => {
- throw new Error('product offline')
- })
- const rows = [
- {
- productId: 'p1',
- productName: '历史产品',
- isToxic: 1,
- brandMode: 'required',
- brandId: 'b1',
- brandName: '品牌'
- }
- ]
- const before = structuredClone(rows)
- await assert.rejects(helper(rows, true), /product offline/)
- assert.deepEqual(rows, before)
- const fallback = await helper(structuredClone(rows))
- assert.equal(fallback[0].brandId, 'b1')
- assert.equal(fallback[0].isToxic, 1)
- })
- }
- for (const copy of [false, true])
- test(`mobile ${copy ? 'copy' : 'edit'} opts into strict product initialization`, async () => {
- const strictArgs: unknown[] = []
- const h = harness({
- copy,
- enrich: async (rows: any, strict: boolean) => {
- strictArgs.push(strict)
- return rows
- },
- overrides: {
- enrichPlanProductLinesAlias: async (rows: any, strict: boolean) => {
- strictArgs.push(strict)
- return rows
- }
- }
- })
- await h.handleOpened()
- assert.deepEqual(strictArgs, [true, true])
- })
- test('delivery slow contract completion cannot clear products selected after fast contract completion', async () => {
- const slow = deferred()
- const h = harness({
- contract: (id: string) => (id === 'slow' ? slow.promise : Promise.resolve({ id, products: [] }))
- })
- await h.handleOpened()
- const watchDelivery = deliveryWatcher(h)
- h.deliveryForms.value = [
- { contracts: [{ contractId: 'slow' }], products: [{ productId: 'original' }] }
- ]
- const first = watchDelivery([{ contractKey: 'slow' }], [{ contractKey: 'original' }])
- h.deliveryForms.value[0].contracts = [{ contractId: 'fast' }]
- await watchDelivery([{ contractKey: 'fast' }], [{ contractKey: 'slow' }])
- h.deliveryForms.value[0].products = [{ productId: 'new-selection' }]
- slow.resolve({ id: 'slow', products: [] })
- await first
- assert.deepEqual(h.deliveryForms.value[0].products, [{ productId: 'new-selection' }])
- })
- for (const mode of ['draft', 'save', 'saveAndArrange', 'rearrange']) {
- for (const newPlan of [false, true]) {
- test(`late ${newPlan ? 'create' : 'update'} ${mode} cannot emit, arrange or clear a new session busy state`, async () => {
- const write = deferred()
- let started = false
- const h = harness({ newPlan })
- await h.handleOpened()
- h.formData.vehicleId = 'v1'
- const apiMethod = newPlan ? 'createFull' : 'update'
- h.planApi[apiMethod] = async () => {
- started = true
- return write.promise
- }
- const saving = h.handleSubmit(mode)
- while (!started) await Promise.resolve()
- await h.handleOpened()
- h.submitLoading.value = true
- h.events.splice(0)
- write.resolve({ id: 'new-plan' })
- await saving
- assert.equal(h.submitLoading.value, true)
- assert.deepEqual(h.events, [])
- assert.deepEqual(h.writes, [])
- })
- }
- }
- for (const stage of ['detail', 'history', 'confirm', 'arrange']) {
- test(`arrange ${stage} completion after unmount cannot continue or emit`, async () => {
- const pending = deferred()
- let started = false
- const pause = async () => {
- started = true
- return pending.promise
- }
- const h = harness({
- realArrange: true,
- overrides:
- stage === 'confirm'
- ? { confirmBeforePlanArrange: pause }
- : stage === 'history'
- ? { loadArrangeHistoricalFlags: pause }
- : stage === 'arrange'
- ? { submitPlanArrange: pause }
- : {}
- })
- await h.handleOpened()
- h.formData.vehicleId = 'v1'
- if (stage === 'detail') h.planApi.get = pause
- else
- h.planApi.get = async () => ({
- id: 'plan-1',
- transportStatus: stage === 'history' ? 'arranged' : 'pending',
- products: []
- })
- const work = h.performArrangeAfterSave('plan-1')
- while (!started) await Promise.resolve()
- h.invalidatePageLoad()
- h.events.splice(0)
- pending.resolve(stage === 'detail' ? { id: 'plan-1', products: [] } : {})
- await work
- assert.deepEqual(h.events, [])
- assert.deepEqual(h.writes, [])
- })
- }
- for (const [file, name] of [
- ['usePlContractToPlan', 'enrichPlanProductLinesAlias'],
- ['usePlToxicProductCertCheck', 'enrichPlanProductsHazardFromCrm']
- ]) {
- for (const response of [{}, { data: null }, { id: 'other' }]) {
- test(`${name} strict rejects malformed 200 ${JSON.stringify(response)} and preserves snapshot`, async () => {
- const helper = enrichmentHelper(file, name, async () => response)
- const rows = [
- {
- productId: 'p1',
- productName: '历史产品',
- isToxic: 1,
- brandMode: 'required',
- brandId: 'brand-1'
- }
- ]
- const before = structuredClone(rows)
- await assert.rejects(helper(rows, true), /产品/)
- assert.deepEqual(rows, before)
- await assert.doesNotReject(helper(structuredClone(rows)))
- })
- }
- }
- for (const response of [
- {},
- null,
- { id: 'other', products: [] },
- { id: 'c1' },
- { id: 'c1', products: {} }
- ]) {
- test(`mobile contract malformed 200 ${JSON.stringify(response)} cannot cache an empty list`, async () => {
- const h = harness({ contract: async () => response })
- await assert.rejects(h.fetchContractProductIds('c1'), /合同/)
- assert.deepEqual(h.contractProductIdsCache.value, {})
- assert.deepEqual(h.invalidations, ['c1'])
- assert.equal(h.pageReady.value, false)
- })
- }
- for (const copy of [false, true])
- test(`mobile ${copy ? 'copy' : 'edit'} HTTP 404 error is actionable Chinese`, async () => {
- const error = Object.assign(new Error('Request failed with status code 404'), {
- response: { status: 404 }
- })
- const h = harness({
- copy,
- get: async () => {
- throw error
- }
- })
- await h.handleOpened()
- assert.equal(h.pageLoadError.value, copy ? '复制来源不存在或已删除' : '运输计划不存在或已删除')
- })
- test('mobile network error is Chinese while backend business message is preserved', async () => {
- for (const [error, expected] of [
- [new Error('Network Error'), '网络连接失败,请检查网络后重新加载'],
- [
- Object.assign(new Error('Request failed with status code 500'), {
- response: { data: { message: '当前租户无权查看此计划' } }
- }),
- '当前租户无权查看此计划'
- ]
- ]) {
- const h = harness({
- get: async () => {
- throw error
- }
- })
- await h.handleOpened()
- assert.equal(h.pageLoadError.value, expected)
- }
- })
- test('mobile error actions meet the 44px minimum tap target', () => {
- const errorStyle = descriptor.styles[0].content.split('.mp-plan-form-page')[0]
- assert.match(errorStyle, /\.mp-plan-load-error[\s\S]*?\.el-button[\s\S]*?min-height:\s*44px/)
- })
- test('obsolete delivery contract error does not close the ready fast selection', async () => {
- const slow = deferred()
- const h = harness({
- contract: (id: string) => (id === 'slow' ? slow.promise : Promise.resolve({ id, products: [] }))
- })
- await h.handleOpened()
- const watchDelivery = deliveryWatcher(h)
- h.deliveryForms.value = [{ contracts: [{ contractId: 'slow' }], products: [] }]
- const first = watchDelivery([{ contractKey: 'slow' }], [{ contractKey: 'original' }])
- h.deliveryForms.value[0].contracts = [{ contractId: 'fast' }]
- await watchDelivery([{ contractKey: 'fast' }], [{ contractKey: 'slow' }])
- h.deliveryForms.value[0].products = [{ productId: 'new-selection' }]
- slow.reject(new Error('old contract error'))
- await first
- assert.equal(h.pageReady.value, true)
- assert.equal(h.pageLoadError.value, '')
- assert.deepEqual(h.deliveryForms.value[0].products, [{ productId: 'new-selection' }])
- })
- test('arrange hazard completion after unmount does not continue to confirmation', async () => {
- const pending = deferred()
- let calls = 0
- let started = false
- let confirmed = false
- const h = harness({
- realArrange: true,
- enrich: async (rows: any) => {
- calls += 1
- if (calls === 1) return rows
- started = true
- return pending.promise
- },
- overrides: {
- confirmBeforePlanArrange: async () => {
- confirmed = true
- }
- }
- })
- await h.handleOpened()
- h.formData.vehicleId = 'v1'
- const work = h.performArrangeAfterSave('plan-1')
- while (!started) await Promise.resolve()
- h.invalidatePageLoad()
- pending.resolve([])
- await work
- assert.equal(confirmed, false)
- assert.deepEqual(h.events, [])
- assert.deepEqual(h.writes, [])
- })
- function loadWatcher(h: ReturnType<typeof harness>) {
- const watcher = ast.statements.find(
- (node) =>
- ts.isExpressionStatement(node) &&
- node.getText(ast).startsWith('watch(') &&
- node.getText(ast).includes('() => loadForm.value.contracts')
- )
- assert.ok(watcher)
- let callback!: (...args: any[]) => Promise<void>
- const bindings = {
- mobileStopIdentity, ...h,
- pageLoadGeneration: 1,
- loadContractFetchSeq: 0,
- filterProductsByAllowedProductIds,
- syncFromLoadProducts: () => {},
- watch: (_source: any, cb: any) => {
- callback = cb
- }
- }
- new Function(
- ...Object.keys(bindings),
- ts.transpile(watcher.getText(ast), { target: ts.ScriptTarget.ESNext })
- )(...Object.values(bindings))
- return callback
- }
- for (const rejects of [false, true])
- test(`load contract old ${rejects ? 'error' : 'success'} cannot affect current selection`, async () => {
- const slow = deferred()
- const h = harness({
- contract: (id: string) =>
- id === 'slow'
- ? slow.promise
- : Promise.resolve({ id, products: [{ productId: 'fast-product' }] })
- })
- await h.handleOpened()
- const watchLoad = loadWatcher(h)
- h.loadForm.value = { contracts: [{ contractId: 'slow' }], products: [] }
- const first = watchLoad([{ contractId: 'slow' }], [{ contractId: 'original' }])
- h.loadForm.value.contracts = [{ contractId: 'fast' }]
- await watchLoad([{ contractId: 'fast' }], [{ contractId: 'slow' }])
- h.loadForm.value.products = [{ productId: 'fast-product' }]
- if (rejects) slow.reject(new Error('obsolete'))
- else slow.resolve({ id: 'slow', products: [{ productId: 'slow-product' }] })
- await first
- assert.deepEqual(h.loadForm.value.products, [{ productId: 'fast-product' }])
- assert.deepEqual(h.loadContractProductIds.value, ['fast-product'])
- assert.equal(h.pageReady.value, true)
- assert.equal(h.pageLoadError.value, '')
- })
- for (const mode of ['draft', 'save', 'saveAndArrange', 'rearrange'])
- test(`desktop ${mode} keeps its save and arrange completion`, async () => {
- const h = harness({ page: false, realArrange: true })
- await h.handleOpened()
- h.formData.vehicleId = 'v1'
- h.invalidatePageLoad()
- await h.handleSubmit(mode)
- assert.equal(h.writes[0][0], 'update')
- assert.equal(
- h.writes.some((row) => row[0] === 'arrange'),
- mode === 'saveAndArrange' || mode === 'rearrange'
- )
- assert.equal(h.events.filter((event) => event[0] === 'success').length, 1)
- assert.equal(h.submitLoading.value, false)
- })
- // Execute the production watcher through Vue's scheduler, together with the real
- // fetchContractProductIds closure from harness(). Only the contract API is stubbed.
- function mountDeliveryWatcher(h: ReturnType<typeof harness>) {
- const watcher = ast.statements.find(
- (node) =>
- ts.isExpressionStatement(node) &&
- node.getText(ast).startsWith('watch(') &&
- node.getText(ast).includes('contractKey: collectStopContractIds')
- )
- assert.ok(watcher)
- const bindings = { ...h, mobileStopIdentity, watch, pageLoadGeneration: 1, deliveryContractFetchSeqByIndex: {} }
- const scope = effectScope()
- scope.run(() =>
- new Function(
- ...Object.keys(bindings),
- ts.transpile(watcher.getText(ast), { target: ts.ScriptTarget.ESNext })
- )(...Object.values(bindings))
- )
- return () => scope.stop()
- }
- async function flushDeliveryWatchers() {
- for (let tick = 0; tick < 30; tick += 1) await nextTick()
- }
- for (const page of [true, false])
- test(`real Vue delivery watcher ${page ? 'mobile' : 'desktop'} clears both changed segments despite crossed requests`, async () => {
- const slow = deferred()
- const h = harness({
- page,
- contract: async (id: string) => {
- if (id === 'new-0') await slow.promise
- return { id, products: [] }
- }
- })
- await h.handleOpened()
- h.deliveryForms.value = [
- { contracts: [{ contractId: 'old-0' }], products: [{ productId: 'old-product-0', contractId: 'old-0' }] },
- { contracts: [{ contractId: 'old-1' }], products: [{ productId: 'old-product-1', contractId: 'old-1' }] }
- ]
- const stop = mountDeliveryWatcher(h)
- try {
- h.deliveryForms.value[0].contracts = [{ contractId: 'new-0' }]
- await nextTick()
- h.deliveryForms.value[1].contracts = [{ contractId: 'new-1' }]
- await nextTick()
- slow.resolve({})
- await flushDeliveryWatchers()
- assert.deepEqual(
- h.deliveryForms.value.map((row: any) => row.products),
- [[], []]
- )
- assert.equal(h.pageLoadError.value, '')
- } finally {
- stop()
- }
- })
- test('real Vue watcher does not let an earlier batched callback supersede a later segment selection', async () => {
- const slow = deferred()
- const fast = deferred()
- const requests: string[] = []
- const h = harness({
- contract: async (id: string) => {
- requests.push(id)
- if (id === 'new-0') await slow.promise
- if (id === 'latest-1') await fast.promise
- return { id, products: [] }
- }
- })
- await h.handleOpened()
- h.deliveryForms.value = [
- { contracts: [{ contractId: 'old-0' }], products: [{ productId: 'old-product-0', contractId: 'old-0' }] },
- { contracts: [{ contractId: 'old-1' }], products: [{ productId: 'old-product-1', contractId: 'old-1' }] }
- ]
- const stop = mountDeliveryWatcher(h)
- try {
- h.deliveryForms.value[0].contracts = [{ contractId: 'new-0' }]
- h.deliveryForms.value[1].contracts = [{ contractId: 'intermediate-1' }]
- await nextTick()
- h.deliveryForms.value[1].contracts = [{ contractId: 'latest-1' }]
- await nextTick()
- slow.resolve({})
- await flushDeliveryWatchers()
- fast.resolve({})
- await flushDeliveryWatchers()
- assert.deepEqual(
- h.deliveryForms.value.map((row: any) => row.products),
- [[], []]
- )
- assert.equal(requests.includes('intermediate-1'), false)
- h.deliveryForms.value[1].products = [{ productId: 'latest-product-1' }]
- await flushDeliveryWatchers()
- assert.deepEqual(h.deliveryForms.value[1].products, [{ productId: 'latest-product-1' }])
- } finally {
- stop()
- }
- })
- test('real Vue watcher preserves newly selected products while another segment finishes later', async () => {
- const slow = deferred()
- const h = harness({
- contract: async (id: string) => {
- if (id === 'new-0') await slow.promise
- return { id, products: [] }
- }
- })
- await h.handleOpened()
- h.deliveryForms.value = [
- { contracts: [{ contractId: 'old-0' }], products: [{ productId: 'old-product-0', contractId: 'old-0' }] },
- { contracts: [{ contractId: 'old-1' }], products: [{ productId: 'old-product-1', contractId: 'old-1' }] }
- ]
- const stop = mountDeliveryWatcher(h)
- try {
- h.deliveryForms.value[0].contracts = [{ contractId: 'new-0' }]
- await nextTick()
- h.deliveryForms.value[1].contracts = [{ contractId: 'new-1' }]
- await flushDeliveryWatchers()
- h.deliveryForms.value[1].products = [{ productId: 'new-selection' }]
- slow.resolve({})
- await flushDeliveryWatchers()
- assert.deepEqual(h.deliveryForms.value[0].products, [])
- assert.deepEqual(h.deliveryForms.value[1].products, [{ productId: 'new-selection' }])
- } finally {
- stop()
- }
- })
- for (const rejects of [false, true])
- test(`real Vue batched delivery changes still process later segments after obsolete first ${rejects ? 'error' : 'success'}`, async () => {
- const slow = deferred()
- const requests: string[] = []
- const h = harness({
- contract: async (id: string) => {
- requests.push(id)
- if (id === 'slow-0') await slow.promise
- return { id, products: [] }
- }
- })
- await h.handleOpened()
- h.deliveryForms.value = [
- { contracts: [{ contractId: 'old-0' }], products: [{ productId: 'old-product-0', contractId: 'old-0' }] },
- { contracts: [{ contractId: 'old-1' }], products: [{ productId: 'old-product-1', contractId: 'old-1' }] }
- ]
- const stop = mountDeliveryWatcher(h)
- try {
- h.deliveryForms.value[0].contracts = [{ contractId: 'slow-0' }]
- h.deliveryForms.value[1].contracts = [{ contractId: 'new-1' }]
- await nextTick()
- h.deliveryForms.value[0].contracts = [{ contractId: 'latest-0' }]
- await flushDeliveryWatchers()
- if (rejects) slow.reject(new Error('obsolete first segment'))
- else slow.resolve({})
- await flushDeliveryWatchers()
- assert.ok(
- requests.includes('new-1'),
- 'the batched later segment must still request its contract'
- )
- assert.deepEqual(
- h.deliveryForms.value.map((row: any) => row.products),
- [[], []]
- )
- assert.equal(h.pageReady.value, true)
- assert.equal(h.pageLoadError.value, '')
- } finally {
- stop()
- }
- })
- test('mobile delivery watcher preserves segment products and notes during reorder and clears only changed contract target', async () => {
- const slow=deferred()
- const h=harness({contract:async(id:string)=>{if(id==='new-a')await slow.promise;return {id,products:[]}}})
- await h.handleOpened()
- h.deliveryForms.value=[
- {customerId:'same',contracts:[{contractId:'old-a'}],products:[{productId:'a',contractId:'old-a'}],notes:'A'},
- {customerId:'same',contracts:[{contractId:'old-b'}],products:[{productId:'b',contractId:'old-b'}],notes:'B'}
- ]
- const stop=mountDeliveryWatcher(h)
- try {
- h.deliveryForms.value[0].contracts=[{contractId:'new-a'}]
- await nextTick()
- h.deliveryForms.value=[{...h.deliveryForms.value[1],seq:1},{...h.deliveryForms.value[0],seq:2}]
- await flushDeliveryWatchers()
- assert.deepEqual(h.deliveryForms.value.map((r:any)=>r.products),[[{productId:'b',contractId:'old-b'}],[{productId:'a',contractId:'old-a'}]])
- slow.resolve({}); await flushDeliveryWatchers()
- assert.deepEqual(h.deliveryForms.value.map((r:any)=>r.products),[[{productId:'b',contractId:'old-b'}],[]])
- assert.deepEqual(h.deliveryForms.value.map((r:any)=>r.notes),['B','A'])
- } finally {stop()}
- })
- test('mobile removed delivery response cannot clear surviving same-customer segment', async () => {
- const slow=deferred(), h=harness({contract:async(id:string)=>{if(id==='new-a')await slow.promise;return {id,products:[]}}})
- await h.handleOpened()
- h.deliveryForms.value=[{customerId:'same',contracts:[{contractId:'old-a'}],products:[{productId:'a'}]},{customerId:'same',contracts:[{contractId:'old-b'}],products:[{productId:'b'}]}]
- const stop=mountDeliveryWatcher(h)
- try {
- h.deliveryForms.value[0].contracts=[{contractId:'new-a'}];await nextTick()
- h.deliveryForms.value=[{...h.deliveryForms.value[1],seq:1}];await flushDeliveryWatchers()
- slow.resolve({});await flushDeliveryWatchers()
- assert.deepEqual(h.deliveryForms.value[0].products,[{productId:'b'}])
- } finally {stop()}
- })
- for(const removed of [false,true]) test(`mobile load contract ${removed?'removal':'replacement with same product'} clears only its products`,async()=>{
- const h=harness({contract:async(id:string)=>({id,products:[{productId:'p'}]})})
- await h.handleOpened()
- h.loadForm.value={contracts:removed?[]:[{contractId:'new'}],products:[{productId:'p',contractProductId:'old-line',contractId:'old'}]}
- const run=loadWatcher(h)
- await run(h.loadForm.value.contracts,[{contractId:'old'}])
- assert.deepEqual(h.loadForm.value.products,[])
- })
- for (const newPlan of [false, true]) test(`old server without capability blocks ${newPlan ? 'new' : 'edit'} page readiness and all writes`, async () => {
- const h = harness({ newPlan, overrides: { assertMobilePersistenceCapability, planApi: { mobileProductCapability: async () => ({}) } } })
- await h.handleOpened()
- assert.equal(h.pageReady.value, false)
- assert.match(h.pageLoadError.value, /服务.*更新/)
- await h.handleSubmit('draft')
- assert.deepEqual(h.writes, [])
- })
- test('successful capability handshake still rejects an old detail response without the explicit marker', async () => {
- const h = harness({ overrides: { assertMobilePersistenceCapability } })
- await h.handleOpened()
- assert.equal(h.pageReady.value, false)
- assert.match(h.pageLoadError.value, /服务.*更新/)
- })
- test('actual copy mapper preserves same-packaging second-load and shared delivery association while clearing source ids', async () => {
- const { mobileLoadProductKey } = await import('../src/views/m/pl/plan/mobilePlanPersistence')
- const product = (id: string, planProductId: string) => ({ id, planProductId, productId: 'p', productNo: 'P', productName: '标准名', packageSpec: '20kg/袋', model: 'M', unit: '吨' })
- const sourcePlan = { id: 'plan-1', load: { products: [product('l1', 'pp1'), product('l2', 'pp2')], contracts: [] }, deliveries: [{ id: 'd1', products: [product('dl1', 'pp2')], contracts: [] }, { id: 'd2', products: [product('dl2', 'pp2')], contracts: [] }], products: [] }
- const h = harness({ copy: true, get: async () => sourcePlan })
- await h.handleOpened()
- assert.equal(h.pageReady.value, true)
- const key = mobileLoadProductKey(h.loadForm.value.products[1])
- assert.deepEqual(h.deliveryForms.value.map((row: any) => row.products[0].mobileLoadProductKey), [key, key])
- assert.ok(h.loadForm.value.products.every((row: any) => !row.id && !row.planProductId))
- assert.ok(h.deliveryForms.value.every((row: any) => !row.id && !row.products[0].id && !row.products[0].planProductId))
- })
- test('capability endpoint 404 explains service upgrade rather than a missing plan', async () => {
- const h = harness({newPlan: true, overrides: {planApi: {mobileProductCapability: async () => {throw Object.assign(new Error('Request failed with status code 404'), {response:{status:404}})}}}})
- await h.handleOpened()
- assert.equal(h.pageReady.value,false)
- assert.match(h.pageLoadError.value,/服务.*更新/)
- assert.doesNotMatch(h.pageLoadError.value,/计划不存在/)
- })
|