Files
client-web/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
T

594 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
const routerSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
const editorShellSource = readFileSync(new URL('../components/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
const progressSource = readFileSync(new URL('../components/OrderProgressPanel.vue', import.meta.url), 'utf8')
const customsDialogSource = readFileSync(new URL('../components/CustomsDocumentsDialog.vue', import.meta.url), 'utf8')
const customsApiSource = readFileSync(new URL('../api/customs.ts', import.meta.url), 'utf8')
test('routes the confirmed first-leg path to the implemented order view', () => {
assert.match(routerSource, /path: 'orders\/first-leg'[\s\S]*?component: \(\) => import\('\.\/views\/FirstLegOrdersView\.vue'\)[\s\S]*?release: 'active'/)
})
test('opens order creation as a dedicated protected page while keeping draft editing in a large dialog', () => {
assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/)
assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?props: \{ createPage: true \}/)
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
assert.match(source, /defineProps<\{ createPage\?: boolean \}>/)
assert.match(source, /const isCreatePage = computed\(\(\) => props\.createPage\)/)
assert.match(source, /<FirstLegOrderEditorShell[^>]*:page="isCreatePage"/)
assert.match(editorShellSource, /<section v-if="page" class="first-leg-editor-page">/)
assert.match(editorShellSource, /<el-dialog[\s\S]*v-else/)
assert.match(editorShellSource, /class="first-leg-editor-dialog"/)
assert.match(editorShellSource, /width="94vw"/)
assert.doesNotMatch(editorShellSource, /<el-drawer/)
assert.match(editorShellSource, /返回订单列表/)
})
test('exposes real list, draft, submit, and detail workflow entries without cancellation', () => {
assert.match(source, /function openCreate\(\)/)
assert.match(source, /async function saveDraft\(\)/)
assert.match(source, /function submitOrder\(\)/)
assert.match(source, /async function openDetail\(order: FirstLegOrder\)/)
assert.match(source, /Promise\.allSettled\(\[/)
assert.match(source, /const orderPageRequest = isCreatePage\.value\s*\? store\.loadWarehouseType\('DOMESTIC', true\)\s*:\s*store\.load\(\{ includeDrafts: true \}\)/)
assert.match(source, /Promise\.allSettled\(\[\s*orderPageRequest,\s*companyRequest/)
assert.match(source, /tenantStore\.loadCompanyContext\(\)/)
assert.match(source, /await store\.loadDetail\(order\.id\)/)
assert.doesNotMatch(source, /cancelOrder|取消订单/)
})
test('keeps create-page support errors scoped to create-page requests', () => {
assert.match(source, /if \(!isCreatePage\.value && store\.draftLoadError\)/)
})
test('blocks order submission before company approval while preserving draft actions', () => {
assert.match(source, /companySubmissionBlocked/)
assert.match(source, /companyAuditLoaded\.value = true/)
assert.match(source, /企业资料待审核,当前仅可保存订单草稿/)
assert.match(source, /暂时无法提交订单/)
assert.match(source, /router\.push\('\/company\/profile'\)/)
assert.match(source, /auth\.hasPermission\('order:first-leg:submit'\)/)
})
test('defers approval checks to the server when the member cannot read the company profile', () => {
assert.match(source, /const canViewCompanyProfile = computed/)
assert.match(source, /const companySubmissionBlocked = computed\(\(\) => companyAuditLoaded\.value/)
assert.match(source, /const knownCompanyAuditStatus = companyAuditLoaded\.value \? tenantStore\.company\.auditStatus : undefined/)
assert.match(source, /store\.submitSaved\(savedOrder\.id, knownCompanyAuditStatus\)/)
})
test('renders dynamic cargo lines and a responsive editor dialog', () => {
assert.match(source, /v-for="\(item, index\) in form\.items"/)
assert.match(source, /function addCargoItem\(\)/)
assert.match(source, /function removeCargoItem\(index: number\)/)
assert.match(editorShellSource, /first-leg-editor-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
assert.match(editorShellSource, /width: 100vw !important; height: 100vh;/)
assert.match(source, /class="mobile-order-list"/)
assert.match(source, /\.desktop-order-table \{ display: none; \}/)
})
test('uses the full editor dialog width for cargo details and cost estimates', () => {
assert.match(source, /:class="\{ 'is-dialog-order-form': !isCreatePage \}"/)
assert.match(source, /\.order-form\.is-dialog-order-form \{ width: 100%; max-width: none; \}/)
assert.match(source, /\.is-dialog-order-form > \.cargo-section, \.is-dialog-order-form > \.cost-estimate-section \{ width: 100%; max-width: none; \}/)
assert.match(source, /\.price-estimate__result \{ display: grid; grid-template-columns:/)
assert.match(source, /\.cargo-table-wrap \{ width: 100%;/)
})
test('keeps primary mobile order actions touch sized', () => {
assert.match(source, /\.editor-actions :deep\(\.el-button\) \{ min-height: 44px;/)
})
test('uses the shared query toolbar and data table without metric cards', () => {
assert.match(source, /import QueryToolbar, \{ type QueryField, type QueryModel \}/)
assert.match(source, /import DataTable, \{ type DataTableColumn \}/)
assert.match(source, /const filters = ref<QueryModel>/)
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders">/)
assert.match(source, /async function searchOrders\(model: QueryModel\)/)
assert.match(source, /await store\.load\(\{/)
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns"/)
assert.match(source, /\{ key: 'actions', label: '操作', width: 216, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' \}/)
assert.doesNotMatch(source, /order-metrics|订单概览/)
})
test('adds prototype download actions to the order query panel', () => {
assert.match(source, /下载唛头/)
assert.match(source, /导出订单信息/)
assert.match(source, /function downloadShippingMarks\(\)/)
assert.match(source, /downloadMerchantOrderCartonLabelsBatch\(orders\.map\(\(order\) => order\.id\)\)/)
assert.match(source, /error\.traceId \? `,追踪号 \$\{error\.traceId\}`/)
assert.doesNotMatch(source, /order\.businessSummary\.inboundId/)
assert.match(source, /批量箱唛-\$\{orders\.length\}单\.pdf/)
assert.match(source, /function exportOrderInformation\(\)/)
assert.match(source, /selectedOrders\.value\.map/)
assert.match(source, /订单信息\.csv/)
assert.doesNotMatch(source, /exportOrderInformation[\s\S]{0,1200}loadDetail/)
})
test('selects orders before downloading marks or exporting last-mile data', () => {
assert.match(source, /\{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true \}/)
assert.match(source, /const selectedOrders = ref<FirstLegOrder\[\]>\(\[\]\)/)
assert.match(source, /const selectedOrderCount = computed\(\(\) => selectedOrders\.value\.length\)/)
assert.match(source, /@selection-change="handleOrderSelectionChange"/)
assert.match(source, /:disabled="selectedOrderCount === 0" @click="downloadShippingMarks"/)
assert.match(source, /:disabled="selectedOrderCount === 0" @click="exportOrderInformation"/)
assert.match(source, /const orders = selectedOrders\.value\s*$/m)
assert.match(source, /const rows = selectedOrders\.value\.map/)
assert.doesNotMatch(source, /const orders = filteredOrders\.value\.filter/)
assert.doesNotMatch(source, /const rows = filteredOrders\.value\.map/)
})
test('downloads selected carton labels with one array request', () => {
const downloadStart = source.indexOf('async function downloadShippingMarks()')
const downloadEnd = source.indexOf('function formatExportDateTime', downloadStart)
const downloadSource = source.slice(downloadStart, downloadEnd)
assert.match(downloadSource, /await downloadMerchantOrderCartonLabelsBatch\(orders\.map\(\(order\) => order\.id\)\)/)
assert.match(downloadSource, /saveBlob\(result\.blob/)
assert.match(downloadSource, /result\.failureCount/)
assert.doesNotMatch(downloadSource, /for \(const order of orders\)/)
assert.doesNotMatch(downloadSource, /Promise\.all\(/)
assert.doesNotMatch(downloadSource, /businessSummary\.inboundId\)/)
assert.doesNotMatch(downloadSource, /没有已生成的入库单|未生成入库单/)
})
test('exports every visible order field with normalized dates', () => {
assert.match(source, /function formatExportDateTime\(value: string \| null \| undefined\)/)
assert.match(source, /return `\$\{date\.getFullYear\(\)\}-\$\{pad\(date\.getMonth\(\) \+ 1\)\}-\$\{pad\(date\.getDate\(\)\)\} \$\{pad\(date\.getHours\(\)\)\}:\$\{pad\(date\.getMinutes\(\)\)\}:\$\{pad\(date\.getSeconds\(\)\)\}`/)
for (const label of ['订单号', '入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '创建时间']) {
assert.match(source, new RegExp(`'${label}'`))
}
assert.match(source, /formatExportDateTime\(order\.inboundedAt\)/)
assert.match(source, /order\.pickupMethod === 'PICKUP' \? formatExportDateTime\(order\.requestedPickupAt\) : '-'/)
assert.match(source, /formatListMetric\(order\.chargeableVolumeCbm, 'm³'\)/)
assert.doesNotMatch(source, /formatExportDateTime\(order\.businessDate\)/)
assert.match(source, /formatExportDateTime\(order\.createdAt\)/)
})
test('matches the prototype with selectable dense order-list columns', () => {
assert.match(source, /aria-label="设置订单列表显示列"/)
assert.match(source, /<SlidersHorizontal :size="18"/)
assert.match(source, /<el-checkbox-group v-model="visibleColumnKeys"/)
assert.match(source, /draggable="true"/)
assert.match(source, /@drop\.prevent="dropOrderColumn/)
assert.match(source, /function moveOrderColumn/)
assert.match(source, /function resetOrderColumns/)
assert.match(source, />恢复默认<\/el-button>/)
assert.match(source, /const visibleOrderColumns = computed/)
assert.match(source, /:columns="visibleOrderColumns"/)
for (const label of ['入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间']) {
assert.match(source, new RegExp(`label: '${label}'`))
}
assert.match(source, /order\.businessSummary\.totalPackageCount/)
assert.match(source, /summarizeFirstLegCargo\(order\.items\)/)
assert.match(source, /order\.items\.reduce/)
assert.match(source, /<template #chargeableWeight>-<\/template>/)
assert.match(source, /<template #billingUnitPrice>-<\/template>/)
assert.match(source, /<template #chargeableVolume="\{ row \}">\{\{ formatListMetric\(\(row as FirstLegOrder\)\.chargeableVolumeCbm, 'm³'\) \}\}<\/template>/)
assert.match(source, /pickupMethod === 'PICKUP'[\s\S]{0,160}requestedPickupAt[\s\S]{0,160}: '-'/)
assert.match(source, /inboundedAt/)
assert.doesNotMatch(source, /label: '业务日期'/)
assert.doesNotMatch(source, /<template #businessDate>/)
assert.match(source, /<template #createdAt="\{ row \}">\{\{ formatDateTime\(\(row as FirstLegOrder\)\.createdAt\) \}\}<\/template>/)
assert.match(source, /order\.skuNos\.length/)
assert.doesNotMatch(source, /pagedOrders[\s\S]{0,100}loadDetail/)
})
test('keeps dense order identifiers on one line and aligns columns by content type', () => {
assert.match(source, /key: 'order'[\s\S]{0,160}showOverflowTooltip: true/)
assert.match(source, /key: 'status'[\s\S]{0,100}align: 'center'[\s\S]{0,100}headerAlign: 'center'/)
assert.match(source, /key: 'grossWeight'[\s\S]{0,100}align: 'right'[\s\S]{0,100}headerAlign: 'right'/)
assert.match(source, /class="order-number-link table-code"/)
assert.doesNotMatch(source, /<template #order="\{ row \}">[\s\S]{0,400}class="order-reference"/)
assert.match(source, /\.table-ellipsis, \.table-multivalue \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
assert.match(source, /\.order-number-link \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
assert.match(source, /\.prototype-order-table :deep\(\.el-table__body-wrapper td\.el-table__cell\) \{ height: 50px;/)
})
test('shows one unified progress line and C02 business summaries without detail history', () => {
assert.match(source, /import OrderProgressPanel/)
assert.match(source, /<OrderProgressPanel :order="detailOrder" \/>/)
assert.match(source, /<h3>关键业务摘要<\/h3>/)
assert.match(source, /progressSummaryItems/)
assert.match(source, /summary\.preloadStatus/)
assert.match(source, /summary\.expectedPickCount/)
assert.match(source, /summary\.deliveryTrackingNo/)
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
assert.match(progressSource, /国内装柜进度及后续履约同步/)
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
assert.doesNotMatch(source, /<h3>报关回执<\/h3>/)
assert.match(source, /船期摘要/)
assert.match(source, /柜次进度/)
assert.doesNotMatch(source, /summary\.sealNo \|\|/)
assert.doesNotMatch(progressSource, /客户确认|差异处理中|派送中/)
})
test('uses only fields present on C02 rows before loading full detail', () => {
assert.match(source, /getOrderCargoSummary/)
assert.match(source, /label: '渠道'/)
assert.match(source, /label: '目的仓'/)
assert.match(source, /displayListValue/)
assert.doesNotMatch(source, /key: 'operator'/)
assert.doesNotMatch(source, /key: 'transportMode', type: 'select'/)
assert.match(source, /store\.draftLoadError/)
assert.match(source, /mainStatus: statusValue && statusValue !== 'DRAFT' \? statusValue : undefined/)
assert.match(source, /draftOnly: statusValue === 'DRAFT'/)
assert.match(source, /createdFrom: range\[0\] \? `\$\{range\[0\]\}T00:00:00`/)
})
test('shows and searches the inbound code after a draft is submitted', () => {
assert.match(source, /label: '入仓码 \/ SKU'/)
assert.match(source, /order\.businessSummary\.inboundCode/)
assert.match(source, /<template #inboundCode="\{ row \}">[\s\S]*?class="inbound-code-link table-ellipsis table-code"/)
assert.match(source, /v-if="order\.businessSummary\.inboundCode" class="order-reference inbound-code-link"/)
assert.match(source, /<dt>入仓码<\/dt><dd><button v-if="detailOrder\.businessSummary\.inboundCode"/)
assert.match(source, /<span v-else>—<\/span>/)
})
test('keeps the seven-stage progress and business summary usable on mobile', () => {
assert.match(progressSource, /grid-template-columns: repeat\(7, 82px\)/)
assert.match(progressSource, /overflow-x: auto/)
assert.match(source, /\.fulfillment-summary \{ grid-template-columns: 1fr; \}/)
})
test('keeps the first viewport compact and moves create into the table toolbar', () => {
const queryToolbarSource = source.slice(source.indexOf('<QueryToolbar'), source.indexOf('</QueryToolbar>') + '</QueryToolbar>'.length)
assert.match(source, /class="order-table-toolbar"/)
assert.match(source, /<strong>订单列表<\/strong><span>共 \{\{ filteredOrders\.length \}\} 条<\/span>/)
assert.match(source, /class="order-table-toolbar"[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/)
assert.match(queryToolbarSource, /<template #actions>[\s\S]*下载唛头[\s\S]*导出订单信息/)
assert.doesNotMatch(queryToolbarSource, /order:first-leg:create|创建订单/)
assert.match(source, /client-main:has\(> \.first-leg-page:not\(\.is-create-route\)\) > \.client-page-heading > div\) \{ position: absolute; width: 1px; height: 1px;/)
})
test('uses the prototype query grid and only shows the qualification notice before approval', () => {
assert.match(source, /\.first-leg-query-toolbar \{ display: block;/)
assert.match(source, /query-toolbar__filters\) \{ display: grid; min-width: 0; grid-template-columns: repeat\(4,/)
assert.match(source, /query-toolbar__actions\) \{ justify-content: flex-start; margin-top: 14px;/)
assert.match(source, /v-if="companySubmissionBlocked && !supportingDataErrors\.some/)
assert.doesNotMatch(source, /企业资料已审核,可正常提交订单/)
assert.doesNotMatch(source, /order-eligibility\.is-approved/)
})
test('does not let order-list loading block the create form', () => {
assert.match(source, /v-loading="isCreatePage && supportingDataLoading"/)
assert.match(source, /const pageDataLoading = computed\(\(\) => supportingDataLoading\.value \|\| \(!isCreatePage\.value && store\.loading\)\)/)
})
test('does not expose arbitrary status mutation controls to customers', () => {
assert.doesNotMatch(source, /setStatusForPreview/)
assert.doesNotMatch(source, /状态更新|更新状态/)
})
test('requires an enabled platform receiving warehouse in the order form', () => {
assert.match(source, /store\.warehouses\.filter\(\(warehouse\) => warehouse\.warehouseType === 'DOMESTIC'\)/)
assert.match(source, /availableWarehouses\.value\.length === 0/)
assert.match(source, /label="国内收货仓"[^>]*required/)
assert.match(source, /v-model="form\.receivingWarehouseId"/)
assert.doesNotMatch(source, /selectedWarehouse\.businessHours/)
assert.doesNotMatch(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__header\) \{[^}]*border-bottom: 1px solid var\(--border\)/)
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__body\) \{[^}]*min-height: 0;[^}]*overflow-y: auto;[^}]*overscroll-behavior: contain/)
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\?\.name/)
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/)
})
test('selects platform destinations and uses customer-scoped server address books', () => {
assert.match(source, /warehouse\.warehouseType === form\.destinationType/)
assert.match(source, /await store\.loadWarehouseType\(type\)/)
assert.match(source, /await loadDestinationWarehouses\(form\.destinationType\)/)
assert.match(source, /:loading="destinationWarehousesLoading"/)
assert.match(source, /v-model="form\.destinationWarehouseId"/)
assert.match(source, /form\.amazonWarehouseCode = form\.destinationType === 'FBA' \? warehouse\.code : ''/)
assert.match(source, /getMerchantOrderAddresses\(\)/)
assert.match(source, /createMerchantOrderAddress\(payload\)/)
assert.match(source, /updateMerchantOrderAddress\(form\.(?:senderAddressId|privateDestinationAddressId), payload\)/)
assert.match(source, /disableMerchantOrderAddress\(id\)/)
assert.match(source, /v-model="form\.senderAddressId"/)
assert.match(source, /v-model="form\.privateDestinationAddressId"/)
assert.doesNotMatch(source, /localStorage|cc:first-leg:private-addresses/)
assert.match(source, /私人地址不能引用平台公共仓库|form\.destinationWarehouseId = ''/)
})
test('lets the create-page instance own destination loading and exposes a recoverable error state', () => {
assert.match(source, /await router\.push\(\{ name: 'first-leg-order-create' \}\)\s*\n\}/)
assert.doesNotMatch(source, /await router\.push\(\{ name: 'first-leg-order-create' \}\)[\s\S]*?await loadDestinationWarehouses\(form\.destinationType\)/)
assert.match(source, /if \(destinationResult\.status === 'rejected'\) supportingDataErrors\.value\.push\(`目的仓加载失败:\$\{errorMessage\(destinationResult\.reason\)\}`\)/)
assert.match(source, /下单配套数据加载失败/)
})
test('shows a loading state before the list can create an order', () => {
assert.match(source, /<el-button v-if="auth\.hasPermission\('order:first-leg:create'\)" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading \|\| availableWarehouses\.length === 0" @click="openCreate"><FilePlus2/)
assert.match(source, /pageDataLoading \? '加载收货仓中' : \(availableWarehouses\.length \? '创建第一笔订单'/)
})
test('auto-fills trusted warehouse and company data instead of asking customers to repeat it', () => {
assert.match(source, /async function selectDestinationWarehouse\(id: string\)/)
assert.match(source, /await store\.loadWarehouseDetail\(warehouse\.id\)/)
assert.match(source, /availableWarehouses\.value\.length === 1/)
assert.match(source, /availableDestinationWarehouses\.value\.length === 1/)
assert.match(source, /function createPickupPartySnapshot\(\)/)
assert.match(source, /contactName: tenantStore\.company\.contactName/)
assert.match(source, /selectedDestinationWarehouse\.address/)
})
test('collects the backend-required first-leg fields', () => {
for (const model of ['destinationCountryCode', 'destinationWarehouseName', 'channelCode', 'pricingTaxMode', 'pickupMethod', 'taxMode', 'destinationType', 'packageType', 'upc']) {
assert.match(source, new RegExp(`v-model="(?:form|item)\\.${model}"`))
}
assert.match(source, /form\.channelName = channel\?\.channelName/)
assert.match(source, /v-model="item\.woodenProduct"/)
assert.match(source, /第 \{\{ woodenCargoLines\.join\('、'\) \}\} 条货物已勾选木制品/)
assert.match(source, /第 \{\{ index \+ 1 \}\} 条须商检/)
})
test('distills order creation into the confirmed prototype sections with server pricing', () => {
const cargoTableSource = source.slice(source.indexOf('<table class="cargo-edit-table">'), source.indexOf('</table>', source.indexOf('<table class="cargo-edit-table">')))
assert.match(source, /<h3>目的地信息<\/h3>/)
assert.match(source, /<h3>客户下单<\/h3>/)
assert.match(source, /<h3>货物明细<\/h3>/)
assert.ok(source.indexOf('<h3>客户下单</h3>') < source.indexOf('<h3>目的地信息</h3>'))
assert.ok(source.indexOf('<h3>目的地信息</h3>') < source.indexOf('<h3>货物明细</h3>'))
assert.match(source, /class="segmented-options"/)
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
assert.match(source, /<table class="cargo-edit-table">/)
assert.match(source, /auth\.hasPermission\('order:first-leg:create'\)[\s\S]*批量导入订单/)
assert.doesNotMatch(source, /create-batch-import-entry|需要一次创建多条订单|页面上方“Excel 批量导入”/)
assert.match(source, /导入成功的数据只创建草稿,不会直接提交审核/)
assert.match(source, /下载正式模板[\s\S]*选择 Excel/)
assert.match(source, /仅支持 \.xlsx,文件不超过 5MB、最多 50 行/)
assert.match(source, /cargo\.woodenProduct(true\/false)[\s\S]*customsInspection 必须填写 true/)
assert.match(source, /下载 Excel 错误报告/)
assert.match(source, /batchImportError[\s\S]*requestId[\s\S]*traceId/)
assert.match(source, /添加行[\s\S]*删除所选行/)
assert.doesNotMatch(source, /货物明细导入模板\.xls/)
for (const heading of ['SKU NO.', '中文品名', '箱规长 cm', '总毛重', '报关 HS CODE', '货件编号', '跟踪编号', 'UPC NO.']) {
assert.ok(cargoTableSource.includes(heading))
}
assert.ok(cargoTableSource.indexOf('包装类型') < cargoTableSource.indexOf('危险品'))
assert.ok(cargoTableSource.indexOf('危险品') < cargoTableSource.indexOf('货件编号'))
assert.ok(cargoTableSource.indexOf('木制品') < cargoTableSource.indexOf('货件编号'))
assert.match(source, /参考运费/)
assert.match(source, /仅供参考,最终以实际结算为准/)
assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/)
})
test('loads only active server channels for the selected domestic warehouse', () => {
assert.match(source, /getMerchantPricingChannels\(warehouseId\)/)
assert.match(source, /const channelOptions = ref<MerchantPricingChannel\[\]>\(\[\]\)/)
assert.match(source, /channelOptions\.value = \[\]/)
assert.match(source, /form\.channelCode = ''/)
assert.match(source, /sequence !== channelRequestSequence \|\| form\.receivingWarehouseId !== warehouseId/)
assert.match(source, /channel\.pricingTaxModes\.includes\(form\.pricingTaxMode\)/)
assert.match(source, /availablePricingTaxModes/)
assert.match(source, /:loading="channelLoading"/)
assert.match(source, /:disabled="!form\.receivingWarehouseId \|\| channelLoading"/)
assert.match(source, /当前收货仓暂无可用渠道/)
assert.match(source, /渠道加载失败/)
assert.match(source, /@click="retryPricingChannels"/)
assert.doesNotMatch(source, /SEA-US-GENERAL|SEA-US-SENSITIVE|SEA-US-EXPRESS|美线 FBA 直送/)
assert.doesNotMatch(source, /channelOptions\.some\(\(channel\) => channel\.code/)
})
test('estimates by warehouse, destination, tax mode, weight and volume without blocking saves', () => {
assert.match(source, /estimateMerchantFirstLegPrice\(request\)/)
assert.match(source, /originWarehouseId: form\.receivingWarehouseId/)
assert.match(source, /destinationCode: form\.destinationType === 'FBA' \? form\.amazonWarehouseCode : undefined/)
assert.match(source, /destinationWarehouseId: form\.destinationType === 'OVERSEAS'/)
assert.match(source, /pricingTaxMode: form\.pricingTaxMode/)
assert.match(source, /actualWeightKg: summary\.grossWeightKg/)
assert.match(source, /actualVolumeCbm: summary\.volumeCbm/)
assert.match(source, /setTimeout\(\(\) => void loadPriceEstimate\(\), 300\)/)
assert.match(source, /sequence === estimateRequestSequence/)
assert.match(source, /您仍可正常保存订单草稿/)
assert.match(source, /v-for="mode in availablePricingTaxModes"/)
assert.match(source, /pricingTaxModeLabel\(mode\)/)
assert.match(source, /priceEstimate\.chargeableQuantity/)
assert.match(source, /priceEstimateBreakdown\.actualLabel/)
assert.match(source, /priceEstimateBreakdown\.convertedLabel/)
assert.match(source, /priceEstimateBreakdown\.basisLabel/)
assert.match(source, /两者取大/)
assert.match(source, /priceEstimate\.unitPrice/)
assert.match(source, /priceEstimate\.estimatedAmount/)
assert.match(source, /priceEstimateDisclaimer/)
assert.doesNotMatch(source, /3,310|2,990|2,500|静态预估展示/)
assert.doesNotMatch(source, /当前价格表不提供不包税报价|历史草稿的不包税设置/)
const saveSource = source.slice(source.indexOf('async function saveDraft()'), source.indexOf('async function submitOrder()'))
assert.doesNotMatch(saveSource, /loadPriceEstimate|priceEstimateError|estimateMerchantFirstLegPrice/)
})
test('keeps the reference estimate readable on narrow screens', () => {
assert.match(source, /\.price-estimate__result \{ grid-template-columns: 1fr; \}/)
assert.match(source, /\.price-estimate__result > small \{ grid-column: auto; \}/)
})
test('shows field-level validation and concise guidance for required order inputs', () => {
assert.match(source, /ref="orderFormRef"[^>]*:model="form" :rules="orderFormRules"/)
assert.match(source, /prop="shipperPartySnapshot\.contactName" required/)
assert.match(source, /prop="consigneePartySnapshot\.address" required/)
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/)
assert.match(source, /:prop="`items\.\$\{index\}\.upc`">\s*<el-input v-model="item\.upc"[^>]*placeholder="UPC NO\.(选填)"/)
assert.doesNotMatch(source, /:prop="`items\.\$\{index\}\.upc`"[^>]*(?:required|:rules)/)
assert.match(source, /UPC NO\. 选填/)
assert.doesNotMatch(source, /UPC必填/)
assert.match(source, /v-model="item\.cargoNameCn"[^>]*placeholder="中文品名"/)
assert.match(source, /\.order-form :deep\(\.el-form-item__error\).*position: static/)
})
test('collects snapshots only for pickup and private destinations', () => {
assert.doesNotMatch(source, /firstLegPartySource|v-model="form\.(?:shipper|consignee)PartyId"/)
assert.match(source, /form\.pickupMethod === 'PICKUP'/)
assert.match(source, /form\.destinationType === 'PRIVATE'/)
assert.match(source, /form\.shipperPartySnapshot\.contactName/)
assert.match(source, /form\.consigneePartySnapshot\.address/)
assert.match(source, /syncPrivateDestinationName/)
assert.match(source, /changePickupMethod/)
})
test('shows fulfillment courier information as read-only client detail', () => {
assert.match(source, /履约快递信息/)
assert.match(source, /detailOrder\.expressCompanyName/)
assert.match(source, /detailOrder\.trackingNumber/)
assert.match(source, /客户端仅只读展示/)
assert.doesNotMatch(source, /v-model="form\.expressCompanyName"/)
assert.doesNotMatch(source, /v-model="form\.trackingNumber"/)
})
test('shows the full customer-safe order detail without platform-only controls', () => {
const detailSource = source.slice(source.indexOf('<el-dialog v-model="detailOpen"'), source.indexOf('</el-dialog>', source.indexOf('<el-dialog v-model="detailOpen"')))
for (const heading of ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
assert.match(source, new RegExp(`<h3>${heading}<\\/h3>`))
}
for (const field of [
'贸易条款', '发货方式', '报关方式', '运输渠道', '收货要求', '目的仓类型', '海关查验',
'提交时间', '完成时间', 'SKU NO.', 'UPC NO.', '报关 HS CODE', '清关 HS CODE', '每箱数量',
'总净重', '清关申报单价', '中英文材质', '中英文用途', '产品规格', '货件编号',
'跟踪编号', '危险品',
]) {
assert.match(source, new RegExp(field))
}
assert.doesNotMatch(detailSource, /承运商|待平台指派/)
assert.match(source, /detailOrder\.customerRemark/)
assert.doesNotMatch(detailSource, /内部备注|审核通过|添加备注/)
})
test('opens order detail in a large dialog and paginates cargoes as a read-only table', () => {
assert.match(source, /<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh"/)
assert.doesNotMatch(source, /<el-drawer v-model="detailOpen"/)
assert.match(source, /const detailCargoPage = ref\(1\)/)
assert.match(source, /const detailCargoPageSize = ref\(5\)/)
assert.match(source, /const pagedDetailCargoItems = computed/)
assert.match(source, /items\.slice\(start, start \+ detailCargoPageSize\.value\)/)
assert.match(source, /<el-table :data="pagedDetailCargoItems"[^>]*class="cargo-detail-table"/)
assert.match(source, /v-model:current-page="detailCargoPage"/)
assert.match(source, /v-model:page-size="detailCargoPageSize"/)
assert.match(source, /:total="detailOrder\.items\.length"/)
assert.doesNotMatch(source, /class="cargo-detail-list"/)
assert.match(source, /first-leg-detail-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
})
test('loads merchant inbound billing as an isolated read-only detail section', () => {
assert.match(source, /import InboundBillingPanel/)
assert.match(source, /import \{ getMerchantInboundBilling \}/)
assert.match(source, /void loadInboundBilling\(order\)/)
assert.match(source, /<InboundBillingPanel/)
assert.match(source, /@retry="retryInboundBilling"/)
assert.match(source, /error instanceof ApiError && error\.code === 'A0404'/)
assert.match(source, /inboundBillingUnavailableMessage\.value = error\.message \|\| '入库及费用结果尚未发布'/)
assert.match(source, /order\.status === 'DRAFT'/)
assert.match(source, /订单提交后可查看入库结果/)
assert.doesNotMatch(source, /confirmMerchantInbound|adjustMerchantInbound|sendMerchantInbound/)
})
test('keeps platform enterprise scope and tenant terminology out of the customer order UI', () => {
const templateSource = source.slice(source.indexOf('<template>'), source.lastIndexOf('</template>'))
assert.doesNotMatch(templateSource, /租户|tenantId|所属企业|企业筛选/)
})
test('discards stale inbound billing responses when the drawer closes or the user changes orders', () => {
assert.match(source, /let inboundBillingRequestSequence = 0/)
assert.match(source, /const sequence = \+\+inboundBillingRequestSequence/)
assert.match(source, /sequence === inboundBillingRequestSequence[\s\S]*detailOpen\.value[\s\S]*detailId\.value === orderId/)
assert.match(source, /watch\(detailOpen,[\s\S]*resetInboundBillingState\(\)/)
})
test('protects unsaved drafts and exposes recoverable load failures', () => {
assert.match(source, /useUnsavedWorkRegistration\(/)
assert.match(source, /routeKey: unsavedRouteKey/)
assert.doesNotMatch(source, /onBeforeRouteLeave|beforeunload/)
assert.match(source, /confirmDiscardChanges/)
assert.match(source, /:before-close="requestCloseEditor"/)
assert.match(source, /store\.loadError/)
assert.match(source, /订单数据加载失败/)
assert.match(source, /订单详情加载失败/)
assert.match(source, /部分订单配套数据加载失败/)
assert.doesNotMatch(source, /往来单位加载失败/)
assert.match(source, /企业资料加载失败/)
assert.match(source, /@click="loadPageData"/)
assert.match(source, /@click="retryDetail"/)
})
test('copies reactive order drafts into plain request data', () => {
assert.match(source, /function cloneDraft\(draft: FirstLegOrderDraft\)/)
assert.match(source, /items: draft\.items\.map\(\(item\) => \(\{ \.\.\.item \}\)\)/)
assert.match(source, /store\.saveDraft\(cloneDraft\(form\)/)
assert.match(source, /editingId\.value = savedOrder\.id/)
assert.match(source, /store\.submitSaved\(savedOrder\.id/)
assert.doesNotMatch(source, /structuredClone\((?:form|order\.)/)
})
test('opens the server-owned inbound-code association from desktop, mobile, and order detail', () => {
assert.match(source, /import InboundCodeAssociationDialog/)
assert.match(source, /function openInboundCodeAssociation\(inboundCode: string\)/)
assert.match(source, /#inboundCode[\s\S]*?openInboundCodeAssociation\(\(row as FirstLegOrder\)\.businessSummary\.inboundCode\)/)
assert.match(source, /class="order-reference inbound-code-link"[\s\S]*?openInboundCodeAssociation\(order\.businessSummary\.inboundCode\)/)
assert.match(source, /<dt>入仓码<\/dt><dd><button[\s\S]*?openInboundCodeAssociation\(detailOrder\.businessSummary\.inboundCode\)/)
assert.match(source, /<InboundCodeAssociationDialog[\s\S]*?:inbound-code="inboundAssociationCode"/)
assert.doesNotMatch(source, /openInboundCodeAssociation\([^)]*\.id\)/)
})
test('opens customer customs documents only for server-confirmed loaded tax-refund orders', () => {
assert.match(source, /order\.status !== 'LOADED'/)
assert.match(source, /item\.orderId === order\.id && item\.taxMode === 'TAX_REFUND'/)
assert.match(source, /getMerchantCustomsForOrder\(order\.id\)/)
assert.match(source, /v-if="eligibleCustomsDetail\(row as FirstLegOrder\)"[\s\S]*?报关资料/)
assert.match(source, /class="mobile-order-card"[\s\S]*?v-if="eligibleCustomsDetail\(order\)"[\s\S]*?openCustomsDocuments\(order\)[\s\S]*?报关资料/)
assert.match(source, /<CustomsDocumentsDialog[\s\S]*?:can-edit="auth\.hasPermission\('order:first-leg:edit'\)"/)
assert.doesNotMatch(customsApiSource, /customs\/cases\//)
})
test('supports the frozen customs document workflow and customer-safe downloads', () => {
for (const path of [
'/merchant/v1/customs/orders/',
'/merchant/v1/customs/loading-plans/',
'/merchant/v1/customs/documents/',
'/inbounds',
'/completion/download',
]) assert.match(customsApiSource, new RegExp(path.replaceAll('/', '\\/')))
assert.match(customsApiSource, /MAX_CUSTOMS_FILE_SIZE = 50 \* 1024 \* 1024/)
assert.match(customsApiSource, /data: \{ inboundIds, expectedVersion: document\.source\.version \}/)
assert.match(customsApiSource, /requestId: error\.requestId/)
assert.match(customsDialogSource, /报关开始后仅可查看和下载,不提供退回或解锁/)
assert.match(customsDialogSource, /detail\.status === 'COMPLETED' && item\.completion/)
assert.match(customsDialogSource, /资料版本历史/)
assert.match(customsDialogSource, /尚未上传退税报关资料/)
assert.match(customsDialogSource, /退税报关资料加载失败/)
assert.doesNotMatch(customsDialogSource, /accept=/)
})
test('uses the global inbound-code policy to require old-system codes during parallel running', () => {
assert.match(source, /getMerchantInboundCodePolicy\(\)/)
assert.match(source, /async function validateEditorForm\(\)[\s\S]*?const policy = await getMerchantInboundCodePolicy\(\)/)
assert.match(source, /if \(policy\.mode === 'AUTO' && !editingId\.value\) form\.inboundCode = ''/)
assert.match(source, /const manualInboundCodeRequired = computed/)
assert.match(source, /v-if="manualInboundCodeRequired" label="旧系统入仓码"/)
assert.match(source, /当前全系统处于旧系统并跑模式/)
assert.match(source, /当前为旧系统并跑模式,请填写旧系统入仓码/)
})
test('shows authoritative warehouse, loading and sailing fields on desktop and narrow screens', () => {
for (const label of ['国内收货仓', '期望出运', '柜号 / 柜次', '船名 / 航次', '预计船期', '更新时间']) {
assert.match(source, new RegExp(label.replace('/', '\\/')))
}
assert.match(source, /businessSummary\.cabinetNo \|\| .*businessSummary\.containerNo/)
assert.match(source, /businessSummary\.vesselName.*businessSummary\.voyageNo/s)
assert.match(source, /businessSummary\.sailingAt/)
})
test('loads complete order detail before editing and rejects an unavailable historical warehouse', () => {
assert.match(source, /async function openEdit\(order: FirstLegOrder\)/)
assert.match(source, /const detailedOrder = await store\.loadDetail\(order\.id\)/)
assert.match(source, /draftFromOrder\(detailedOrder\)/)
assert.match(source, /draft\.receivingWarehouseId = ''/)
assert.match(source, /draft\.channelCode = ''/)
assert.match(source, /原平台收货仓当前不可用,请重新选择可用收货仓/)
assert.match(source, /原目的仓当前不可用,请重新选择平台维护的目的仓/)
})