import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' const source = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/pages/OceanBookingDetailView.vue'), 'utf8') const routesSource = readFileSync(resolve(process.cwd(), 'src/modules/ocean-bookings/routes.ts'), 'utf8') describe('OceanBookingDetailView draft and release contract', () => { it('recognizes create and DRAFT detail routes as editable', () => { expect(source).toContain("route.name === 'ocean-booking-create' || id.value === 'new'") expect(source).toContain("const editable = computed(() => isNew.value || detail.value?.status === 'DRAFT')") expect(source).toContain("platform:firstleg:ocean-booking:edit") expect(routesSource.indexOf("path: 'ocean-bookings/new'")).toBeLessThan(routesSource.indexOf("path: 'ocean-bookings/:bookingId'")) }) it('uses only DRAFT and RELEASED actions and removes voyage-change/cancel paths', () => { expect(source).toContain("detail.value?.status === 'DRAFT'") expect(source).toContain('删除草稿') expect(source).toContain('放舱') expect(source).not.toContain('申请改船') expect(source).not.toContain('取消订舱') expect(source).not.toContain('change-request') expect(source).not.toContain('取消订舱') expect(source).not.toContain('cancelOceanBooking') }) it('invalidates the cached booking list after deleting a draft', () => { expect(source).toContain("workspace.bumpCacheEpoch('ocean-bookings')") expect(source).toContain("await router.replace('/ocean-bookings')") }) it('saves incomplete DRAFT data and sends null for blank nullable fields', () => { expect(source).toContain('保存草稿') expect(source).toContain('function draftPayload(): OceanBookingDraftData') expect(source).toContain('channelCode: form.data.channelCode?.trim() || null') expect(source).toContain('warehouseId: form.data.warehouseId?.trim() || null') expect(source).toContain('parties: form.data.parties.map') expect(source).toContain('containerRequirements: form.data.containerRequirements.map') expect(source).not.toContain('登记后直接确认') }) it('renders editable container requirements with fixed specifications', () => { expect(source).toContain('aria-label="柜型需求"') expect(source).toContain('containerRequirements') expect(source).toContain('数量') expect(source).toContain('v-model="requirement.quantity"') expect(source).toContain('selectContainerRequirementType(requirement') expect(source).toContain('{{ requirement.capacityCbm }} m³') expect(source).toContain('{{ requirement.maxWeightKg }} kg') expect(source).not.toContain('v-model="container.containerNo"') expect(source).not.toContain('新增真实柜') }) it('performs the complete release gate without the extra explanatory line', () => { expect(source).toContain('function releaseValidationError()') expect(source).toContain('预计到港时间不能早于预计开船时间') expect(source).toContain('放舱前请填写起运港代码或名称') expect(source).toContain('放舱前请填写目的港代码或名称') expect(source).toContain("[form.data.placeOfDelivery, '交货地点']") expect(source).not.toContain("[form.data.customerId, '客户']") expect(source).toContain("if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'") expect(source).not.toContain('请先保存草稿后再放舱') expect(source).toContain('放舱前请填写${missing[1]}') expect(source).not.toContain('放舱后运输资料和预报货量不能修改、不能删除;实际货量可另行补录并记录原因。请确认运输资料无误。') expect(source).toContain('if (hasUnsavedDraftChanges.value)') expect(source).toContain('const saved = await persistDraft(false)') expect(source).toContain('releaseVersion = saved.version') expect(source).toContain('releaseOceanBooking(id.value, { expectedVersion: releaseVersion })') expect(source.indexOf('const saved = await persistDraft(false)')).toBeLessThan(source.indexOf('releaseOceanBooking(id.value, { expectedVersion: releaseVersion })')) expect(source).toContain('if (!saved) return') expect(source).toContain("{{ form.data.billOfLadingNo || '—' }}") expect(source).not.toContain("{{ detail?.billOfLadingNo || '—' }}") expect(source).toContain('const workflowBusy = computed(() => saving.value || actionSaving.value || actualSaving.value || arrivalSaving.value)') expect(source).toContain(':disabled="workflowBusy" @click="openReleaseDialog"') expect(source).not.toContain('releaseReason') expect(source).not.toContain('放舱前请上传有效的订舱确认文件或提单文件') expect(source).not.toContain('OceanBookingDocumentsPanel') }) it('freezes the selected carrier standard code into the booking draft', () => { expect(source).toContain('form.data.carrierCode = partner?.carrierCode ?? null') expect(source).toContain('carrierCode: value.carrierCode ?? selectedCarrier?.carrierCode ?? null') expect(source).toContain('form.data.shippingCompanyId && !form.data.carrierCode') expect(source).toContain('form.data.carrierCode = selected?.carrierCode ?? null') }) it('keeps warehouse-scoped channel and server-filtered booking-agent selectors accessible', () => { expect(source).toContain("warehouseType: 'DOMESTIC', status: 'ACTIVE'") expect(source).toContain('listWarehouseLogisticsChannels') expect(source).toContain('placeholder="单客户可选择,多客户可留空"') expect(source).toContain('当前船公司暂无可用订舱代理') expect(source).toContain(':label="supplier.name"') expect(source).toContain('filterable clearable') expect(source).toContain(':disabled="!formWritable || !form.data.warehouseId || channelsLoading || !!channelsError || channelUnavailable"') expect(source).toContain(':disabled="!editable || !form.data.carrierCode || bookingAgentsLoading || !!bookingAgentsError"') }) it('loads booking agents from the carrier-filtered backend contract and clears an invalid selection', () => { expect(source).toContain('listOceanBookingAgents(carrierCode)') expect(source).toContain('shouldClearBookingAgent(form.data.bookingAgentId, options)') expect(source).toContain('clearBookingAgent()') expect(source).toContain("form.data.carrierCode ? '请选择订舱代理' : '请先选择船公司'") expect(source).toContain('') expect(source).not.toContain('listSuppliers(') expect(source).not.toContain('isActiveBookingAgent') }) it('keeps dense layout, operation feedback and narrow-screen behavior', () => { expect(source).toContain('') expect(source).toContain('LoadErrorState') expect(source).toContain('@media(max-width:720px)') expect(source).toContain('@media(max-width:420px)') expect(source).toContain('OceanBookingPrintDialog') expect(source).toContain('打印托运单') expect(source).toContain('打印提单确认件') }) it('keeps a DRAFT detail read-only when the user lacks write permission', () => { expect(source).toContain('const formWritable = computed(() => canSave.value)') expect(source).toContain(':disabled="!formWritable" class="booking-groups density-form"') expect(source).toMatch(/\.booking-core\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1\.2fr\) minmax\(0,\s*1fr\)/) expect(source).toMatch(/\.booking-group:last-child\s*\{\s*grid-column:\s*1\s*\/\s*-1/) expect(source).toMatch(/\.container-table-wrap\s*\{[^}]*max-width:\s*100%;\s*overflow-x:\s*auto/) expect(source).toContain('v-if="formWritable" class="container-actions"') expect(source).toContain(':disabled="!formWritable"') }) it('carries the existing read-only CarrierScheduleQuery on the booking detail page', () => { expect(source).toContain('CarrierScheduleQueryPanel') expect(source).toContain("from '@/modules/port-schedules'") expect(source).toContain(':ocean-booking-id="detail.id"') expect(source).not.toContain('createCarrierScheduleQuery') }) })