73 lines
6.9 KiB
TypeScript
73 lines
6.9 KiB
TypeScript
import { afterEach, expect, it, vi } from 'vitest'
|
||
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
||
import FleetQuotesView from './FleetQuotesView.vue'
|
||
import TripOperationsView from './TripOperationsView.vue'
|
||
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||
import TripOperationsCharts from '../components/TripOperationsCharts.vue'
|
||
import * as quotes from '../api/fleetQuotes'
|
||
import * as reports from '../api/tripOperations'
|
||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (p: string) => p.endsWith(':list') }) }))
|
||
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
||
vi.mock('../api/fleetQuotes', () => ({ listFleetQuotes: vi.fn(), setFleetQuoteStatus: vi.fn(), deleteFleetQuote: vi.fn(), exportFleetQuotes: vi.fn(), saveFleetCsv: vi.fn() }))
|
||
vi.mock('../api/tripOperations', () => ({ listTripOperations: vi.fn(), exportTripOperations: vi.fn() }))
|
||
const wrappers: VueWrapper[] = []
|
||
const layout = { template: '<div><slot name="metrics"/><slot name="state"/><slot/></div>' }
|
||
function mount(component: object) { const wrapper = shallowMount(component, { global: { stubs: { AdminPage: layout, ListPageLayout: layout, QueryToolbar: true,
|
||
DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '<div><header><slot name="toolbar-start"/><slot name="toolbar-actions"/></header><section class="filters"><slot name="filters"/></section><slot name="notices"/></div>' }, ElButton: { template: '<button><slot/></button>' },
|
||
ElTooltip: true, LoadErrorState: true, FleetQuoteEditor: true, FleetQuoteImport: true, ApiErrorMeta: true } } }); wrappers.push(wrapper); return wrapper }
|
||
afterEach(() => { wrappers.splice(0).forEach(w => w.unmount()); vi.clearAllMocks() })
|
||
it('quotes read-only view fetches a page once and hides all write/export tools', async () => {
|
||
vi.mocked(quotes.listFleetQuotes).mockResolvedValue({ records: [], total: 0, page: 1, size: 20, pages: 0 })
|
||
const wrapper = mount(FleetQuotesView); await flushPromises()
|
||
expect(quotes.listFleetQuotes).toHaveBeenCalledTimes(1); expect(wrapper.text()).not.toContain('新增报价'); expect(wrapper.text()).not.toContain('批量导入')
|
||
expect(wrapper.findComponent({ name: 'DataTable' }).props('columns')[0]).toMatchObject({ key: 'actions', fixed: 'left' })
|
||
const tabs = wrapper.get('header .workbench-list-tabs').findAll('[role="tab"]')
|
||
expect(tabs.map(tab => tab.text())).toEqual(['车队采购报价', '外接销售报价'])
|
||
expect(wrapper.get('.filters').findComponent(QueryToolbar).exists()).toBe(true)
|
||
await tabs[1]!.trigger('click'); await flushPromises()
|
||
expect(quotes.listFleetQuotes).toHaveBeenLastCalledWith(expect.objectContaining({ kind: 'SALE', page: 1 }))
|
||
expect(tabs[1]!.attributes('aria-selected')).toBe('true')
|
||
})
|
||
it('operations uses server totals rather than displayed page size', async () => {
|
||
vi.mocked(reports.listTripOperations).mockResolvedValue({ page: { records: [], total: 21, page: 1, size: 20, pages: 2 }, summary: { trips: 21, inTransit: 12, completed: 9, physicalPallets: 40, palletSpaces: 40, ownPackages: 70, externalPackages: 10, externalAgreedAmounts: [], costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } }, charts: { physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] } })
|
||
const wrapper = mount(TripOperationsView); await flushPromises()
|
||
expect(reports.listTripOperations).toHaveBeenCalledTimes(1); expect(wrapper.text()).not.toContain('导出CSV')
|
||
expect(wrapper.find('#operations-statistics-details').exists()).toBe(false)
|
||
expect(wrapper.findComponent(TripOperationsCharts).props('detailsVisible')).toBe(false)
|
||
await wrapper.find('[aria-controls="operations-statistics-details"]').trigger('click')
|
||
expect(wrapper.text()).toContain('实际出车 21 次')
|
||
expect(wrapper.findComponent(TripOperationsCharts).props('detailsVisible')).toBe(true)
|
||
expect(reports.listTripOperations).toHaveBeenCalledTimes(1)
|
||
await wrapper.find('[aria-controls="operations-statistics-details"]').trigger('click')
|
||
expect(wrapper.find('#operations-statistics-details').exists()).toBe(false)
|
||
wrapper.findComponent(TripOperationsCharts).vm.$emit('show-details'); await flushPromises()
|
||
expect(wrapper.find('#operations-statistics-details').exists()).toBe(true)
|
||
expect(reports.listTripOperations).toHaveBeenCalledTimes(1)
|
||
expect(wrapper.findComponent(TripOperationsCharts).props('summary').physicalPallets).toBe(40)
|
||
expect(wrapper.find('.operations-overview').element.compareDocumentPosition(wrapper.findComponent({ name: 'DataTable' }).element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||
expect(wrapper.findComponent({ name: 'DataTable' }).props('columns').find((column: { key: string }) => column.key === 'finance')).toMatchObject({ defaultVisible: false })
|
||
})
|
||
it('operations queries charts and records together and resets with only one request', async () => {
|
||
vi.mocked(reports.listTripOperations).mockResolvedValue({ page: { records: [], total: 0, page: 1, size: 20, pages: 0 }, summary: { trips: 0, inTransit: 0, completed: 0, physicalPallets: 0, palletSpaces: 0, ownPackages: 0, externalPackages: 0, externalAgreedAmounts: [], costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } }, charts: { physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] } })
|
||
const wrapper = mount(TripOperationsView); await flushPromises()
|
||
const initial = vi.mocked(reports.listTripOperations).mock.calls[0]![0]
|
||
const toolbar = wrapper.findComponent(QueryToolbar)
|
||
toolbar.vm.$emit('update:modelValue', { from: '2026-10-01', to: '2026-10-07', keyword: 'TRIP', sourceType: 'EXTERNAL', status: 'IN_TRANSIT' })
|
||
toolbar.vm.$emit('search'); await flushPromises()
|
||
expect(reports.listTripOperations).toHaveBeenLastCalledWith(expect.objectContaining({ from: '2026-10-01T00:00:00Z', to: '2026-10-08T00:00:00.000Z', keyword: 'TRIP', sourceType: 'EXTERNAL', status: 'IN_TRANSIT' }))
|
||
expect(wrapper.text()).toContain('2026-10-01 至 2026-10-07(UTC)')
|
||
toolbar.vm.$emit('update:modelValue', { from: '2026-09-01', to: '2026-09-07' }); await flushPromises()
|
||
expect(wrapper.text()).toContain('2026-10-01 至 2026-10-07(UTC)')
|
||
toolbar.vm.$emit('reset'); toolbar.vm.$emit('search'); await flushPromises()
|
||
expect(reports.listTripOperations).toHaveBeenCalledTimes(3)
|
||
expect(reports.listTripOperations).toHaveBeenLastCalledWith(initial)
|
||
expect(toolbar.props('modelValue')).toMatchObject({ keyword: '', sourceType: '', status: '' })
|
||
})
|
||
it('operations shows query failures without leaving stale statistics', async () => {
|
||
vi.mocked(reports.listTripOperations).mockRejectedValue(new Error('统计暂不可用'))
|
||
const wrapper = mount(TripOperationsView); await flushPromises()
|
||
expect(wrapper.findComponent(TripOperationsCharts).exists()).toBe(false)
|
||
expect(wrapper.findComponent({ name: 'LoadErrorState' }).attributes('message')).toBe('运营记录加载失败')
|
||
expect(wrapper.text()).not.toContain('实际出车 0 次')
|
||
})
|