Merge remote-tracking branch 'origin/release/test' into dev

This commit is contained in:
2026-09-02 10:34:07 +08:00
80 changed files with 2219 additions and 591 deletions
@@ -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'
@@ -19,9 +20,9 @@ vi.mock('@/modules/iam', () => ({
}))
const row: TruckAppointmentResource = {
id: '9001', appointmentNo: 'TA-9001', status: 'DISPATCHED', orderNo: 'ORD-1', oceanBookingNo: 'OB-1', containerBookingId: '77', cabinetNo: 'CAB-1',
containerNo: 'CONT-1', warehouseName: '义乌仓', shippingCompanyName: '船公司A', bookingAgentName: '代理A', carrierName: '宁波运输', driverName: '王强', vehicleNo: '浙B12345',
plannedLoadingAt: '2026-08-10T10:00:00', payableAmount: '3200.00', receivableAmount: '3800.00', attachmentCount: 2, attachments: [],
id: '9001', appointmentNo: 'TA-9001', status: 'DISPATCHED', orderNo: 'ORD-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1',
warehouseName: '义乌仓', shippingCompanyName: '船公司A', bookingAgentName: '代理A', carrierName: '宁波运输', driverName: '王强', vehicleNo: '浙B12345',
plannedLoadingAt: '2026-08-10T10:00:00', payableAmount: '3200.00', currency: 'RMB', attachmentCount: 2, attachments: [],
}
describe('TruckAppointmentsView', () => {
@@ -37,15 +38,13 @@ describe('TruckAppointmentsView', () => {
it('loads the real list and renders relationship snapshots', async () => {
const wrapper = mount(TruckAppointmentsView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '', containerBookingId: '', containerType: '' })
expect(listTruckAppointments).toHaveBeenCalledWith({ page: 1, size: 20, keyword: '', status: '' })
expect(wrapper.text()).toContain('TA-9001')
expect(wrapper.text()).toContain('CONT-1')
expect(wrapper.text()).toContain('CAB-1')
expect(wrapper.text()).toContain('OB-1')
expect(wrapper.text()).toContain('BL-1')
expect(wrapper.text()).toContain('义乌仓')
expect(wrapper.text()).toContain('船公司A')
expect(wrapper.text()).toContain('代理A')
expect(wrapper.text()).not.toContain('77')
expect(wrapper.text()).toContain('宁波运输')
expect(wrapper.find('button[aria-label="查看 TA-9001"]').exists()).toBe(true)
})
@@ -64,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)")
})
@@ -93,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)")
@@ -125,10 +166,12 @@ describe('TruckAppointmentsView', () => {
expect(source).not.toContain("key: 'customerName'")
expect(source).toContain("key: 'updatedAt'")
expect(source).toContain("key: 'payableAmount', label: '应付拖车费'")
expect(source).toContain("key: 'receivableAmount', label: '应收拖车费'")
expect(source.indexOf("key: 'store'")).toBeLessThan(source.indexOf("key: 'payableAmount'"))
expect(source.indexOf("key: 'payableAmount'")).toBeLessThan(source.indexOf("key: 'receivableAmount'"))
expect(source.indexOf("key: 'receivableAmount'")).toBeLessThan(source.indexOf("key: 'plannedLoadingAt'"))
expect(source.indexOf("key: 'payableAmount'")).toBeLessThan(source.indexOf("key: 'plannedLoadingAt'"))
expect(source).not.toContain('receivableAmount')
expect(source).not.toContain('应收拖车费')
expect(source).not.toContain("key: 'containerNo'")
expect(source).not.toContain("key: 'cabinetNo'")
expect(source).toContain("toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })")
expect(source).toContain("key: 'attachmentCount'")
})