import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { downloadPlatformExportTask, getPlatformExportTask, listPlatformExportTasks, retryPlatformExportTask, type PlatformExportTask, } from '@/modules/downloads/api/platformExportTasks' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAuthStore } from '@/core/auth/auth' import DownloadCenterView from './DownloadCenterView.vue' vi.mock('@/modules/downloads/api/platformExportTasks', () => ({ downloadPlatformExportTask: vi.fn(), getPlatformExportTask: vi.fn(), listPlatformExportTasks: vi.fn(), retryPlatformExportTask: vi.fn(), })) class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} } const task: PlatformExportTask = { id: '9', taskNo: 'EXP9', exportType: 'CUSTOMER_LIST', format: 'XLSX', taskName: '客户列表', status: 'SUCCEEDED', totalRows: 12, processedRows: 12, fileName: '客户列表.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', sizeBytes: 2048, failureMessage: null, requestedBy: '1', requestedUsername: 'admin', requestedAt: '2026-08-03T10:00:00', startedAt: '2026-08-03T10:00:01', completedAt: '2026-08-03T10:00:02', expiresAt: '2026-08-10T10:00:02', } enableAutoUnmount(afterEach) afterEach(() => vi.useRealTimers()) describe('DownloadCenterView', () => { beforeEach(() => { vi.clearAllMocks() document.body.innerHTML = '' vi.stubGlobal('ResizeObserver', ResizeObserverStub) Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:export') }) Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() }) vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) const pinia = createPinia() setActivePinia(pinia) const auth = useAuthStore() auth.token = 'token' auth.permissions = ['platform:download:list', 'platform:customer:export'] useAccessPreviewStore().initializeAfterLogin() vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [task], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(downloadPlatformExportTask).mockResolvedValue(new Blob(['xlsx'])) vi.mocked(getPlatformExportTask).mockResolvedValue(task) vi.mocked(retryPlatformExportTask).mockResolvedValue({ ...task, status: 'PENDING' }) }) it('lists completed tasks and downloads the backend-generated artifact', async () => { const pinia = setActivePinia(createPinia()) const auth = useAuthStore() auth.token = 'token' auth.permissions = ['platform:download:list', 'platform:customer:export'] useAccessPreviewStore().initializeAfterLogin() mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect(document.body.textContent).toContain('客户列表') expect(document.querySelector('.data-table')?.textContent).not.toContain('失败原因') expect(document.querySelector('.data-table')?.textContent).toContain('格式') expect(document.querySelector('.data-table')?.textContent).toContain('完成时间') expect(document.querySelector('.data-table')?.textContent).toContain('过期时间') expect(document.querySelector('.failure-cell')).toBeNull() const button = [...document.querySelectorAll('button')].find((item) => item.textContent?.trim() === '下载') button?.click() await flushPromises() expect(downloadPlatformExportTask).toHaveBeenCalledWith('9') expect(URL.createObjectURL).toHaveBeenCalled() expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledOnce() const clickedAnchor = vi.mocked(HTMLAnchorElement.prototype.click).mock.instances[0] as HTMLAnchorElement expect(clickedAnchor.download).toBe('客户列表.xlsx') await new Promise((resolve) => window.setTimeout(resolve, 0)) expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:export') }) it('shows retry only for failed tasks with the source export permission', async () => { vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [{ ...task, status: 'FAILED', failureMessage: 'IAM timeout' }], page: 1, size: 20, total: 1, pages: 1 }) const pinia = setActivePinia(createPinia()) const auth = useAuthStore() auth.token = 'token' auth.permissions = ['platform:download:list', 'platform:customer:export'] useAccessPreviewStore().initializeAfterLogin() mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect(document.querySelector('.failure-cell')).toBeNull() const button = [...document.querySelectorAll('button')].find((item) => item.textContent?.trim() === '重试') button?.click() await flushPromises() expect(retryPlatformExportTask).toHaveBeenCalledWith('9') }) it('labels and retries Finance tasks with their exact source export permission', async () => { const financeTask = { ...task, exportType: 'FINANCE_RECONCILIATION_STATEMENT_LIST', taskName: '客户对账单', status: 'FAILED' as const, } vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [financeTask], page: 1, size: 20, total: 1, pages: 1 }) const pinia = setActivePinia(createPinia()) const auth = useAuthStore() auth.token = 'token' auth.permissions = ['platform:download:list', 'platform:finance:customer-statement:export'] useAccessPreviewStore().initializeAfterLogin() mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect(document.body.textContent).toContain('客户对账单') const retry = [...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '重试') retry?.click() await flushPromises() expect(retryPlatformExportTask).toHaveBeenCalledWith('9') }) it('loads a fresh task detail from the backend', async () => { const wrapper = mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() const button = [...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '详情') button?.click() await flushPromises() expect(getPlatformExportTask).toHaveBeenCalledWith('9') expect(document.body.textContent).toContain('任务详情 · EXP9') expect(document.body.textContent).toContain('客户列表.xlsx') expect(document.querySelector('.el-drawer')).toBeNull() expect(document.querySelector('.el-dialog')).not.toBeNull() const detailDownload = [...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '下载文件') detailDownload?.click() await flushPromises() expect(downloadPlatformExportTask).toHaveBeenCalledWith('9') wrapper.unmount() }) it('renders an actionable list error and can recover', async () => { vi.mocked(listPlatformExportTasks) .mockRejectedValueOnce(new Error('network down')) .mockResolvedValueOnce({ records: [task], page: 1, size: 20, total: 1, pages: 1 }) mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() expect(document.querySelector('[role="alert"]')?.textContent).toContain('下载任务暂时无法加载') const reload = [...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '重新加载') reload?.click() await flushPromises() expect(listPlatformExportTasks).toHaveBeenCalledTimes(2) expect(document.body.textContent).toContain('共 1 个任务') }) it('shows a clear empty state and hides retry without the source export permission', async () => { vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 }) const pinia = setActivePinia(createPinia()) const auth = useAuthStore() auth.token = 'token' auth.permissions = ['platform:download:list'] useAccessPreviewStore().initializeAfterLogin() mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect(document.body.textContent).toContain('暂无符合筛选条件的下载任务') expect([...document.querySelectorAll('button')] .some((item) => item.textContent?.trim() === '重试')).toBe(false) }) it('keeps failure details out of the list and does not expose retry without the source export permission', async () => { vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [{ ...task, status: 'FAILED', failureMessage: 'IAM timeout' }], page: 1, size: 20, total: 1, pages: 1, }) const pinia = setActivePinia(createPinia()) const auth = useAuthStore() auth.token = 'token' auth.permissions = ['platform:download:list'] useAccessPreviewStore().initializeAfterLogin() vi.mocked(getPlatformExportTask).mockResolvedValue({ ...task, status: 'FAILED', failureMessage: 'IAM timeout' }) mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } }) await flushPromises() expect(document.body.textContent).not.toContain('IAM timeout') expect([...document.querySelectorAll('button')] .some((item) => item.textContent?.trim() === '重试')).toBe(false) ;[...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '详情')?.click() await flushPromises() expect(document.body.textContent).toContain('IAM timeout') }) it('announces the initial loading state while the task request is pending', async () => { let resolveRequest!: (value: { records: PlatformExportTask[]; page: number; size: number; total: number; pages: number }) => void vi.mocked(listPlatformExportTasks).mockReturnValue(new Promise((resolve) => { resolveRequest = resolve })) const wrapper = mount(DownloadCenterView, { global: { plugins: [createPinia(), ElementPlus] } }) await wrapper.vm.$nextTick() expect(wrapper.find('.table-panel').attributes('aria-busy')).toBe('true') resolveRequest({ records: [], page: 1, size: 20, total: 0, pages: 0 }) await flushPromises() expect(wrapper.find('.table-panel').attributes('aria-busy')).toBe('false') }) it('does not schedule another refresh after an in-flight load finishes on an unmounted page', async () => { vi.useFakeTimers() let resolveRequest!: (value: { records: PlatformExportTask[]; page: number; size: number; total: number; pages: number }) => void vi.mocked(listPlatformExportTasks).mockReturnValue(new Promise((resolve) => { resolveRequest = resolve })) const wrapper = mount(DownloadCenterView, { global: { plugins: [createPinia(), ElementPlus] } }) wrapper.unmount() resolveRequest({ records: [{ ...task, status: 'RUNNING' }], page: 1, size: 20, total: 1, pages: 1 }) await flushPromises() await vi.advanceTimersByTimeAsync(5000) expect(listPlatformExportTasks).toHaveBeenCalledTimes(1) }) it('keeps the last successful task list and continues polling after a background refresh failure', async () => { vi.useFakeTimers() vi.mocked(listPlatformExportTasks) .mockResolvedValueOnce({ records: [{ ...task, status: 'RUNNING', processedRows: 3 }], page: 1, size: 20, total: 1, pages: 1 }) .mockRejectedValueOnce(new Error('temporary network failure')) .mockResolvedValueOnce({ records: [{ ...task, status: 'SUCCEEDED', processedRows: 12 }], page: 1, size: 20, total: 1, pages: 1 }) mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() await vi.advanceTimersByTimeAsync(5000) await flushPromises() expect(document.body.textContent).toContain('自动刷新暂时失败') expect(document.body.textContent).toContain('客户列表') await vi.advanceTimersByTimeAsync(5000) await flushPromises() expect(listPlatformExportTasks).toHaveBeenCalledTimes(3) expect(document.body.textContent).not.toContain('自动刷新暂时失败') }) it('updates an open task detail from the latest polling result', async () => { vi.useFakeTimers() vi.mocked(listPlatformExportTasks) .mockResolvedValueOnce({ records: [{ ...task, status: 'RUNNING', processedRows: 3 }], page: 1, size: 20, total: 1, pages: 1 }) .mockResolvedValueOnce({ records: [{ ...task, status: 'SUCCEEDED', processedRows: 12 }], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getPlatformExportTask).mockResolvedValue({ ...task, status: 'RUNNING', processedRows: 3 }) mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() ;[...document.querySelectorAll('button')] .find((item) => item.textContent?.trim() === '详情')?.click() await flushPromises() expect(document.body.textContent).toContain('3 / 12 行') await vi.advanceTimersByTimeAsync(5000) await flushPromises() expect(document.body.textContent).toContain('12 / 12 行') }) })