594 lines
38 KiB
JavaScript
594 lines
38 KiB
JavaScript
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, /原目的仓当前不可用,请重新选择平台维护的目的仓/)
|
||
})
|