114 lines
8.2 KiB
JavaScript
114 lines
8.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
|
|
const adminPage = readFileSync(new URL('../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue', import.meta.url), 'utf8')
|
|
const adminTest = readFileSync(new URL('../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts', import.meta.url), 'utf8')
|
|
const adminTypes = readFileSync(new URL('../../../apps/admin-web/src/modules/ocean-bookings/types.ts', import.meta.url), 'utf8')
|
|
const prototype = readFileSync(new URL('../../../docs/悦/货代SAAS平台后台/prototype-src/modules/06-containers.html', import.meta.url), 'utf8')
|
|
const dto = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java', import.meta.url), 'utf8')
|
|
const openapi = readFileSync(new URL('../../firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml', import.meta.url), 'utf8')
|
|
const schema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql', import.meta.url), 'utf8')
|
|
const unifiedSql = readFileSync(new URL('../../deploy/sql/头程数据库统一增量-MySQL8.sql', import.meta.url), 'utf8')
|
|
const fixture = readFileSync(new URL('./ocean-booking-fixture.mjs', import.meta.url), 'utf8')
|
|
const bookingModule = readFileSync(new URL('./ocean-bookings.mjs', import.meta.url), 'utf8')
|
|
const containerLoading = readFileSync(new URL('./phase3-container-loading.mjs', import.meta.url), 'utf8')
|
|
|
|
const releaseFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']
|
|
|
|
test('prototype evidence keeps service term freight term and optional customer fields', () => {
|
|
assert.match(prototype, /主要运输条款 Service Term \*<\/label><select><option>CY-CY<\/option>/)
|
|
assert.match(prototype, /运费条款 Freight Term \*<\/label><select><option>NINGBO<\/option>/)
|
|
assert.match(prototype, /<label>客户\/公司名称<\/label><input id="booking-customer"/)
|
|
})
|
|
|
|
test('Admin keeps source-backed free text terms without invented option dictionaries', () => {
|
|
assert.match(adminPage, /v-model="form\.data\.serviceTerm"[^>]*maxlength="32"/)
|
|
assert.match(adminPage, /v-model="form\.data\.freightTerm"[^>]*maxlength="32"/)
|
|
assert.doesNotMatch(adminPage, /serviceTermOptions|freightTermOptions/)
|
|
for (const invented of ['CY-DOOR', 'DOOR-CY', 'DOOR-DOOR', 'PREPAID', 'COLLECT']) {
|
|
assert.doesNotMatch(adminPage, new RegExp(invented), `${invented} must not be an invented Admin option`)
|
|
}
|
|
assert.match(adminTest, /not\.toContain\('serviceTermOptions'\)/)
|
|
assert.match(adminTest, /not\.toContain\('freightTermOptions'\)/)
|
|
})
|
|
|
|
test('service and freight terms are required across Java OpenAPI and SQL', () => {
|
|
assert.match(dto, /@NotBlank @Size\(max = 32\) String serviceTerm/)
|
|
assert.match(dto, /@NotBlank @Size\(max = 32\) String freightTerm/)
|
|
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
|
|
assert.match(required, /serviceTerm/)
|
|
assert.match(required, /freightTerm/)
|
|
assert.match(openapi, /serviceTerm: \{type: string, maxLength: 32, description: Required free-text Service Term snapshot/)
|
|
assert.match(openapi, /freightTerm: \{type: string, maxLength: 32, description: Required free-text Freight Term snapshot/)
|
|
assert.doesNotMatch(openapi, /(?:serviceTerm|freightTerm): \{[^\n]*nullable: true/)
|
|
for (const sql of [schema, unifiedSql]) {
|
|
assert.match(sql, /service_term VARCHAR\(32\) NOT NULL/)
|
|
assert.match(sql, /freight_term VARCHAR\(32\) NOT NULL/)
|
|
}
|
|
})
|
|
|
|
test('customer is optional across Admin DraftData DTO and OpenAPI', () => {
|
|
assert.match(adminPage, /label="客户(可选)"[\s\S]*?filterable clearable/)
|
|
assert.doesNotMatch(adminPage, /customerId:\s*\[\{\s*required:\s*true/)
|
|
assert.match(adminTypes, /customerId:\s*string \| null;\s*customerName:\s*string \| null/)
|
|
assert.match(dto, /record DraftData\(@Positive Long customerId,\s*@Size\(max = 128\) String customerName/)
|
|
assert.doesNotMatch(dto, /record DraftData\(@NotNull @Positive Long customerId/)
|
|
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
|
|
assert.ok(required, 'BLOCKED_CONTRACT: DraftData.required is missing from OpenAPI')
|
|
assert.doesNotMatch(required, /customerId|customerName/)
|
|
assert.match(openapi, /customerId: \{[^\n]*nullable: true/)
|
|
assert.match(openapi, /customerName: \{[^\n]*nullable: true/)
|
|
})
|
|
|
|
test('offline-confirmed registration requires transport values while release keeps the frozen five-field snapshot', () => {
|
|
const draftType = adminTypes.slice(adminTypes.indexOf('export interface OceanBookingDraftData'), adminTypes.indexOf('export interface OceanBookingResource'))
|
|
for (const field of releaseFields) {
|
|
assert.match(adminPage, new RegExp(`v-model="form\\.data\\.${field}"`),
|
|
`${field} must be entered when registering an offline-confirmed booking`)
|
|
assert.match(draftType, new RegExp(`${field}\\??:`), `${field} must belong to OceanBookingDraftData`)
|
|
}
|
|
assert.match(adminTest, /线下订舱资料已登记并确认/)
|
|
assert.match(adminTest, /登记后直接确认/)
|
|
const saveRegion = adminPage.slice(adminPage.indexOf('async function save'), adminPage.indexOf('function requireReason'))
|
|
assert.doesNotMatch(saveRegion, /data:\s*form\.data/)
|
|
const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || ''
|
|
for (const field of releaseFields) assert.match(required, new RegExp(field))
|
|
assert.match(dto, /@NotBlank @Size\(max = 128\) String billOfLadingNo/)
|
|
assert.match(dto, /@NotBlank @Size\(max = 128\) String vesselName/)
|
|
assert.match(dto, /@NotBlank @Size\(max = 64\) String voyageNo/)
|
|
assert.match(dto, /@NotNull LocalDateTime estimatedSailingAt/)
|
|
assert.match(dto, /@NotNull LocalDateTime estimatedArrivalAt/)
|
|
const releaseCall = adminPage.slice(adminPage.indexOf('releaseOceanBooking(id.value'), adminPage.indexOf('releaseDialogOpen.value = false'))
|
|
for (const field of releaseFields) assert.match(releaseCall, new RegExp(field))
|
|
})
|
|
|
|
test('HTTP fixtures cover unassigned CONFIRMED persistence and assigned customer isolation', () => {
|
|
const unassignedFlow = bookingModule.slice(bookingModule.indexOf('async function createConfirmedUnassignedBooking'), bookingModule.indexOf('async function releaseLoadingBooking'))
|
|
assert.match(fixture, /oceanBookingDraftWithoutCustomer/)
|
|
assert.match(fixture, /delete draft\.customerId[\s\S]*delete draft\.customerName/)
|
|
assert.match(fixture, /serviceTerm: 'CY-CY', freightTerm: 'NINGBO'/)
|
|
assert.doesNotMatch(fixture, /serviceTerm: 'CY\/CY'|freightTerm: 'PREPAID'/)
|
|
assert.match(bookingModule, /createdResources\.oceanBookingUnassignedCustomer/)
|
|
assert.match(bookingModule, /未指定客户订舱保持空客户且 CONFIRMED 不自动生成内部柜/)
|
|
assert.match(bookingModule, /按真实订舱号查询未指定客户订舱/)
|
|
assert.match(bookingModule, /keyword=\$\{encodeURIComponent\(bookingNo\)\}/)
|
|
assert.doesNotMatch(unassignedFlow, /keyword=\$\{encodeURIComponent\(context\.marker\)\}/)
|
|
assert.doesNotMatch(bookingModule, /keyword=\$\{encodeURIComponent\(context\.marker\)\}/)
|
|
assert.match(bookingModule, /指定客户订舱持久化本轮客户范围快照/)
|
|
assert.match(bookingModule, /phase3Loading[\s\S]*phase3ContainerLoading/)
|
|
assert.match(containerLoading, /source\?\.marker !== context\.marker/)
|
|
assert.match(containerLoading, /id\(candidate\?\.accountId\) === tenantId/)
|
|
assert.match(containerLoading, /id\(item\.accountId\) === tenantId/)
|
|
})
|
|
|
|
test('independent ocean booking release reuses the server-frozen transport snapshot', () => {
|
|
assert.match(bookingModule, /const releaseSnapshotResponse = await request\(context, 'GET', path\)/)
|
|
assert.match(bookingModule, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
|
|
assert.match(bookingModule, /const frozenTransportValid = transportFields\.every/)
|
|
assert.match(bookingModule, /if \(!frozenTransportValid\) return/)
|
|
assert.match(bookingModule, /expectedVersion: version, \.\.\.frozenTransport, reason: context\.marker/)
|
|
assert.doesNotMatch(bookingModule, /expectedVersion: version, billOfLadingNo: `BL-\$\{suffix\}`/)
|
|
assert.doesNotMatch(bookingModule, /estimatedSailingAt: '2026-09-01T08:00:00'/)
|
|
})
|