test(admin): cover booking-scoped trucking

This commit is contained in:
2026-09-01 16:35:40 +08:00
parent 5706645032
commit 4ad8537113
8 changed files with 98 additions and 13 deletions
@@ -67,6 +67,14 @@ describe('TruckAppointmentDetailView status contract', () => {
expect(source).toContain('v-if="relatedError"')
})
it('never exposes container identity or operator database ids', () => {
expect(source).not.toContain('cabinetNo')
expect(source).not.toContain('containerNo')
expect(source).not.toContain('containerBookingId')
expect(source).not.toContain('loadingPlanId')
expect(source).not.toContain('item.operatorId')
})
it('reloads a cached detail after an error without showing stale resource data', () => {
expect(source).toContain("resource.value = null; bookingReadiness.value = null; history.value = []; relatedError.value = ''")
expect(source).toContain('onActivated(() => {')
@@ -92,7 +92,7 @@ onActivated(() => {
<LoadErrorState v-if="error && !resource" title="详情加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<template v-else-if="resource">
<section class="surface-panel form-panel"><TruckAppointmentForm v-model="form" :resource="resource" :readonly="!canEdit" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /></section>
<section class="surface-panel history-section"><h3>状态历史</h3><el-empty v-if="!history.length" description="暂无状态历史" :image-size="48" /><ol v-else><li v-for="(item, index) in history" :key="item.id || `${item.createdAt}-${index}`"><strong>{{ truckAppointmentActionLabel(item.action) }}</strong><span>{{ truckAppointmentStatusLabel(item.fromStatus) }} → {{ truckAppointmentStatusLabel(item.toStatus) }}</span><small>{{ item.operatorName || item.operatorId || '系统' }} · {{ item.createdAt || item.occurredAt || '—' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></section>
<section class="surface-panel history-section"><h3>状态历史</h3><el-empty v-if="!history.length" description="暂无状态历史" :image-size="48" /><ol v-else><li v-for="(item, index) in history" :key="item.id || `${item.createdAt}-${index}`"><strong>{{ truckAppointmentActionLabel(item.action) }}</strong><span>{{ truckAppointmentStatusLabel(item.fromStatus) }} → {{ truckAppointmentStatusLabel(item.toStatus) }}</span><small>{{ item.operatorName || '系统' }} · {{ item.createdAt || item.occurredAt || '—' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></section>
<TruckAttachmentDialog v-model="attachmentOpen" :appointment-id="id" :attachments="resource.attachments || []" :can-manage="canManageAttachments" @changed="loadRelated" />
</template>
</DetailPageLayout>
@@ -2,6 +2,7 @@ import ElementPlus from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http'
import TruckAppointmentsView from './TruckAppointmentsView.vue'
import { listTruckAppointments } from '../api/platformTrucking'
import type { TruckAppointmentResource } from '../types'
@@ -62,10 +63,10 @@ describe('TruckAppointmentsView', () => {
const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.find('button[aria-label="查看 9002"]').exists()).toBe(true)
expect(wrapper.find('button[aria-label="查看拖车预约详情"]').exists()).toBe(true)
const labels = wrapper.findAll('button[aria-label]').map((button) => button.attributes('aria-label'))
expect(labels.every((label) => Boolean(label) && !/undefined|null/.test(label ?? ''))).toBe(true)
expect(labels.every((label) => !label?.includes('9002'))).toBe(true)
expect(source).toContain("appointmentActionLabel('编辑', row as TruckAppointmentResource)")
})
@@ -91,6 +92,48 @@ describe('TruckAppointmentsView', () => {
expect(source.match(/empty-text=/g)).toHaveLength(1)
})
it('renders the server empty state', async () => {
vi.mocked(listTruckAppointments).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 1 })
const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('暂无符合筛选条件的拖车预约')
})
it('marks the table busy while the list request is pending', async () => {
let resolveLoad!: (value: Awaited<ReturnType<typeof listTruckAppointments>>) => void
vi.mocked(listTruckAppointments).mockImplementationOnce(() => new Promise((resolve) => { resolveLoad = resolve }))
const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.find('section[aria-busy="true"]').exists()).toBe(true)
resolveLoad({ records: [row], page: 1, size: 20, total: 1, pages: 1 })
await flushPromises()
expect(wrapper.find('section[aria-busy="false"]').exists()).toBe(true)
})
it('keeps a recoverable load error and retries without losing the page', async () => {
vi.mocked(listTruckAppointments).mockRejectedValueOnce(new ApiResponseError('候选服务暂不可用', 'A0503', 503, undefined, 'req-truck', 'trace-truck'))
const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('候选服务暂不可用')
expect(wrapper.text()).toContain('req-truck')
vi.mocked(listTruckAppointments).mockResolvedValueOnce({ records: [row], page: 1, size: 20, total: 1, pages: 1 })
await wrapper.get('button').trigger('click')
await flushPromises()
expect(listTruckAppointments).toHaveBeenCalledTimes(2)
expect(wrapper.text()).toContain('TA-9001')
})
it('shows create only with the independent create permission', async () => {
let wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).not.toContain('新增拖车')
wrapper.unmount()
useAuthStore().permissions = ['platform:firstleg:truck-appointment:list', 'platform:firstleg:truck-appointment:create']
wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('新增拖车')
})
it('shows the two frequent row actions directly without a more menu', () => {
expect(source).not.toContain('platform:firstleg:truck-appointment:view')
expect(source).toContain("access.can('platform:firstleg:truck-appointment:edit') && ['DRAFT', 'PENDING_DISPATCH', 'RESCHEDULED'].includes(row.status)")
@@ -47,9 +47,7 @@ function formatAmount(value?: string | null, currency?: string | null) {
return currency ? `${display} ${currency}` : display
}
function appointmentActionLabel(action: '查看' | '编辑', row: TruckAppointmentResource) {
const identifier = [row.appointmentNo, row.id]
.map((value) => String(value ?? '').trim())
.find(Boolean)
const identifier = String(row.appointmentNo ?? '').trim()
return identifier ? `${action} ${identifier}` : `${action}拖车预约详情`
}
function statusLabel(value: unknown) {