132 lines
8.7 KiB
JavaScript
132 lines
8.7 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
import ts from 'typescript'
|
|
|
|
const apiSource = readFileSync(new URL('./api/deliveryOrders.ts', import.meta.url), 'utf8')
|
|
const routesSource = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8')
|
|
const listSource = readFileSync(new URL('./views/DeliveryOrdersView.vue', import.meta.url), 'utf8')
|
|
const detailSource = readFileSync(new URL('./views/DeliveryOrderDetailView.vue', import.meta.url), 'utf8')
|
|
const deliveryStyles = readFileSync(new URL('./views/deliveryOrders.css', import.meta.url), 'utf8')
|
|
const permissionSource = readFileSync(new URL('../tenant-management/contracts.ts', import.meta.url), 'utf8')
|
|
|
|
async function importTypeScriptModule(source) {
|
|
const compiled = ts.transpileModule(source, {
|
|
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
|
}).outputText
|
|
return import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
|
}
|
|
|
|
for (const signedPackages of [null, 0, 3]) test(`preserves actual signed quantity ${signedPackages} in merchant list and detail`, async () => {
|
|
const api = await importTypeScriptModule(apiSource.replace(
|
|
"import { apiBlobRequest, apiRequest } from '@/api/http'",
|
|
`const apiBlobRequest = async () => new Blob([])
|
|
const order = { deliveryNo: 'DL-1', packageCount: 9, signedPackages: ${JSON.stringify(signedPackages)} }
|
|
const apiRequest = async (config) => config.url.endsWith('/DL-1')
|
|
? { order, skus: [], fees: [] } : { records: [order], page: 1, size: 20, total: 1, pages: 1 }`,
|
|
))
|
|
assert.equal((await api.listDeliveryOrders({ page: 1, size: 20 })).records[0].signedPackages, signedPackages)
|
|
assert.equal((await api.getDeliveryOrder('DL-1')).signedPackages, signedPackages)
|
|
})
|
|
|
|
test('uses only authenticated merchant delivery endpoints without customer scope or mock fallback', () => {
|
|
assert.match(apiSource, /method: 'GET'[\s\S]*url: '\/merchant\/v1\/last-leg\/delivery-orders'/)
|
|
assert.match(apiSource, /delivery-orders\/\$\{encodeURIComponent\(deliveryNo\)\}/)
|
|
assert.doesNotMatch(apiSource, /method: '(?:POST|PUT|PATCH|DELETE)'|createDeliveryOrder|DeliveryTransferPayload|DeliveryTransferResult/)
|
|
assert.match(apiSource, /apiRequest<DeliveryOrderDetailResponse>/)
|
|
assert.match(apiSource, /\.then\(\(\{ order, skus, fees \}\) => \(\{[\s\S]*\.\.\.order,[\s\S]*fees,[\s\S]*items: skus/)
|
|
assert.doesNotMatch(apiSource, /customerId|tenantId|Record<string, unknown>|mock|fallback/i)
|
|
})
|
|
|
|
test('keeps customer delivery list and read-only detail active without a creation route', () => {
|
|
const listRoute = routesSource.match(/path: 'orders\/delivery',[\s\S]*?\n\s*},/)?.[0] ?? ''
|
|
assert.match(listRoute, /permission: 'merchant:warehouse:delivery:list'/)
|
|
assert.match(listRoute, /navigation: \{ key: 'orders-delivery', icon: Truck \}/)
|
|
assert.doesNotMatch(routesSource, /orders\/delivery\/create|orders-delivery-create|DeliveryOrderCreateView/)
|
|
assert.match(routesSource, /path: 'orders\/delivery\/:deliveryNo'[\s\S]*DeliveryOrderDetailView\.vue[\s\S]*props: true[\s\S]*permission: 'merchant:warehouse:delivery:list'/)
|
|
assert.match(routesSource, /orders-delivery-detail[\s\S]*keepAlive: false/)
|
|
})
|
|
|
|
test('renders the operation column first and covers loading, empty, error and pagination states', () => {
|
|
const operationIndex = listSource.indexOf("label: '操作'")
|
|
const deliveryNumberIndex = listSource.indexOf("label: '派送单号'")
|
|
assert.ok(operationIndex >= 0 && operationIndex < deliveryNumberIndex)
|
|
assert.match(listSource, /:loading="loading"/)
|
|
assert.match(listSource, /empty-text="暂无派送单"/)
|
|
assert.match(listSource, /delivery-state--error/)
|
|
assert.match(listSource, /@click="loadOrders">重试/)
|
|
assert.match(listSource, /listDeliveryOrders\(buildQuery\(\)\)/)
|
|
assert.match(listSource, /<DataTable/)
|
|
assert.match(listSource, /v-model:page="page"/)
|
|
assert.match(listSource, /v-model:page-size="pageSize"/)
|
|
assert.match(listSource, /watch\(\[page, pageSize\]/)
|
|
assert.match(apiSource, /\.then\(normalizeDeliveryPage\)/)
|
|
assert.match(apiSource, /total: Number\(data\.total\)/)
|
|
assert.doesNotMatch(listSource, /row\.id[^a-zA-Z]|deliveryNo \|\| row\.id|customerId|tenantId/)
|
|
})
|
|
|
|
test('does not expose customer delivery creation even to an account retaining an old permission snapshot', () => {
|
|
assert.doesNotMatch(listSource, /delivery:create|canCreate|openCreate|orders-delivery-create|创建派送单/)
|
|
assert.equal(existsSync(new URL('./views/DeliveryOrderCreateView.vue', import.meta.url)), false)
|
|
})
|
|
|
|
test('loads a typed detail route by delivery number and never exposes an internal id fallback', () => {
|
|
assert.match(detailSource, /defineProps<\{ deliveryNo: string \}>/)
|
|
assert.match(detailSource, /getDeliveryOrder\(props\.deliveryNo\)/)
|
|
assert.match(detailSource, /派送单详情加载失败/)
|
|
for (const heading of ['订单概览', '运输与预约', '货物明细', '费用明细']) assert.match(detailSource, new RegExp(heading))
|
|
assert.doesNotMatch(detailSource, /detail\?\.id|deliveryNo \|\| detail\?\.id|customerId|tenantId/)
|
|
})
|
|
|
|
test('receipt list and binary download use the fixed merchant scope without category or revision escape', async () => {
|
|
const api = await importTypeScriptModule(apiSource.replace(
|
|
"import { apiBlobRequest, apiRequest } from '@/api/http'",
|
|
`export const requests = []
|
|
const apiRequest = async (config) => { requests.push(config); return { deliveryNo: 'DL/1', files: [] } }
|
|
const apiBlobRequest = async (config) => { requests.push(config); return new Blob(['receipt-bytes']) }`,
|
|
))
|
|
assert.deepEqual(await api.getDeliveryReceipts('DL/1'), { deliveryNo: 'DL/1', files: [] })
|
|
assert.equal(await (await api.downloadDeliveryReceipt('DL/1', '123')).text(), 'receipt-bytes')
|
|
assert.deepEqual(api.requests, [
|
|
{ method: 'GET', url: '/merchant/v1/last-leg/delivery-orders/DL%2F1/documents/receipt' },
|
|
{ method: 'GET', url: '/merchant/v1/last-leg/delivery-orders/DL%2F1/documents/receipt/files/123/download' },
|
|
])
|
|
assert.match(detailSource, /empty-text="暂无签收资料"/)
|
|
assert.match(detailSource, /receiptsError[\s\S]*@click="loadReceipts">重试/)
|
|
assert.match(detailSource, /:loading="receiptsLoading"/)
|
|
assert.match(detailSource, /downloadDeliveryReceipt\(props\.deliveryNo, file\.fileId\)/)
|
|
assert.match(detailSource, /URL\.revokeObjectURL\(url\)/)
|
|
assert.match(detailSource, /DELIVERING: '派送中'/)
|
|
for (const status of ['WAIT_DELIVERY', 'DELIVERING', 'DELIVERED', 'REJECTED', 'RETURN_PENDING', 'RETURNED']) {
|
|
assert.match(listSource, new RegExp(`value: '${status}'`))
|
|
}
|
|
assert.doesNotMatch(listSource, /value: '(?:PENDING|IN_TRANSIT|SIGNED)'/)
|
|
assert.doesNotMatch(detailSource, /el-upload|revisionVersion|documents\/BOL/)
|
|
})
|
|
|
|
test('renders a compact customer-readable delivery detail surface', () => {
|
|
assert.match(detailSource, /<article class="delivery-detail-surface">/)
|
|
assert.match(detailSource, /<el-button link type="primary" @click="backToList"><ArrowLeft[^>]*\/>返回列表<\/el-button>/)
|
|
assert.match(detailSource, /class="client-status" :data-tone="statusTone\(detail\.status\)"/)
|
|
assert.match(detailSource, /TRUCK_PALLET: '卡派打托'/)
|
|
assert.match(detailSource, /WAREHOUSE_PICKUP: '仓库自提'/)
|
|
assert.match(detailSource, /PALLET: '托盘'/)
|
|
assert.match(detailSource, /TRUCK: '卡车'/)
|
|
assert.match(detailSource, /SELF: '自有账号'/)
|
|
assert.match(detailSource, /formatter: tableValueFormatter/)
|
|
assert.match(detailSource, /<DataTable :data="items" :columns="itemColumns"/)
|
|
assert.match(detailSource, /<DataTable :data="detail\.fees" :columns="feeColumns"/)
|
|
assert.doesNotMatch(detailSource, /<h2[^>]*>派送单详情<\/h2>|display\(detail\.status\)/)
|
|
assert.match(deliveryStyles, /delivery-detail-surface \.delivery-detail-grid div\{display:grid;min-height:42px;/)
|
|
})
|
|
|
|
test('keeps merchant delivery reads and removes creation from the advertised permission map', async () => {
|
|
assert.match(permissionSource, /'merchant:warehouse:delivery:list': \['merchant:warehouse:delivery:list'\]/)
|
|
assert.doesNotMatch(permissionSource, /merchant:warehouse:delivery:create/)
|
|
const contracts = await importTypeScriptModule(permissionSource)
|
|
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:list', new Set(['merchant:warehouse:delivery:list'])), true)
|
|
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:list', new Set(['merchant:warehouse:delivery:create'])), false)
|
|
assert.equal(Object.hasOwn(contracts.FRONTEND_TO_MERCHANT_PERMISSION_CODES, 'merchant:warehouse:delivery:create'), false)
|
|
assert.equal(contracts.hasMappedMerchantPermission('merchant:warehouse:delivery:create', new Set(['merchant:warehouse:delivery:list'])), false)
|
|
})
|