import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import AppointmentCargoPanel from './AppointmentCargoPanel.vue' import { listPlanningCargo } from './planningApi' vi.mock('./planningApi', () => ({ listPlanningCargo: vi.fn(), resolvePlanningSource: vi.fn() })) vi.mock('vue-router', () => ({ onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn(), useRouter: () => ({ push: vi.fn() }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) const empty = { records: [], page: 1, size: 20, total: 0, pages: 0 } function render(attach = false) { return mount(AppointmentCargoPanel, { attachTo: attach ? document.body : undefined, global: { stubs: { DataTable: { name: 'DataTable', props: ['data', 'total', 'resultsVisible'], emits: ['update:page'], template: '
' }, QueryToolbar: { name: 'QueryToolbar', emits: ['search'], template: '' }, LoadErrorState: true, ApiErrorMeta: true, RowActions: true, AppointmentExternalCandidates: { name: 'AppointmentExternalCandidates', template: '
' }, } } }) } describe('appointment planning cargo query', () => { beforeEach(() => { vi.resetAllMocks(); vi.mocked(listPlanningCargo).mockResolvedValue(empty) }) it('switches cargo tabs by click and keyboard without reloading first-leg cargo', async () => { const wrapper = render(true); await flushPromises() const tabs = () => wrapper.findAll('[role="tab"]') expect(tabs().map(tab => tab.text())).toEqual(['头程货物', '外接货物']) expect(wrapper.find('.integrated-query-group [role="tablist"]').exists()).toBe(true) expect(tabs()[0]!.attributes('aria-selected')).toBe('true') await tabs()[1]!.trigger('click') expect(tabs()[1]!.attributes('aria-selected')).toBe('true') expect(wrapper.findComponent({ name: 'AppointmentExternalCandidates' }).exists()).toBe(true) await tabs()[1]!.trigger('keydown', { key: 'ArrowRight' }) await flushPromises() expect(tabs()[0]!.attributes('aria-selected')).toBe('true') expect(document.activeElement).toBe(tabs()[0]!.element) expect(tabs()[1]!.attributes('tabindex')).toBe('-1') await tabs()[0]!.trigger('keydown', { key: 'End' }) await flushPromises() expect(tabs()[1]!.attributes('aria-selected')).toBe('true') expect(document.activeElement).toBe(tabs()[1]!.element) await tabs()[1]!.trigger('keydown', { key: 'Home' }) expect(tabs()[0]!.attributes('aria-selected')).toBe('true') expect(listPlanningCargo).toHaveBeenCalledTimes(1) wrapper.unmount() }) it('uses one server-page request per entry, page change and reset', async () => { const wrapper = render(); await flushPromises() expect(listPlanningCargo).toHaveBeenCalledTimes(1) await wrapper.get('[data-next]').trigger('click'); await flushPromises() expect(listPlanningCargo).toHaveBeenLastCalledWith({ page: 2, size: 20, keyword: undefined }, expect.any(AbortSignal)) await wrapper.get('[data-search]').trigger('click'); await flushPromises() expect(listPlanningCargo).toHaveBeenCalledTimes(3) expect(listPlanningCargo).toHaveBeenLastCalledWith({ page: 1, size: 20, keyword: undefined }, expect.any(AbortSignal)) }) it('shows a source failure instead of an authoritative empty list', async () => { vi.mocked(listPlanningCargo).mockRejectedValueOnce(new Error('目录不可用')) const wrapper = render(); await flushPromises() expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(true) expect(wrapper.findComponent({ name: 'DataTable' }).props('resultsVisible')).toBe(false) }) it('discards a late previous page and uses the new server total', async () => { let resolveOld!: (value: typeof empty) => void vi.mocked(listPlanningCargo).mockImplementationOnce(() => new Promise(resolve => { resolveOld = resolve })) const wrapper = render() vi.mocked(listPlanningCargo).mockResolvedValueOnce({ ...empty, page: 2, total: 21, pages: 2 }) await wrapper.get('[data-next]').trigger('click'); await flushPromises() resolveOld({ ...empty, total: 100, pages: 5 }); await flushPromises() expect(wrapper.findComponent({ name: 'DataTable' }).props('total')).toBe(21) expect(vi.mocked(listPlanningCargo).mock.calls[0]![1]!.aborted).toBe(true) }) })