Files
admin-web/src/modules/downloads/pages/DownloadCenterView.test.ts
T

293 lines
14 KiB
TypeScript

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, attemptCount: 1, maxAttempts: 3, nextRetryAt: 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<HTMLButtonElement>('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 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<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
button?.click()
await flushPromises()
expect(retryPlatformExportTask).toHaveBeenCalledWith('9')
})
it('shows waiting retry and allows an immediate retry during automatic backoff', async () => {
vi.mocked(listPlatformExportTasks).mockResolvedValue({
records: [{ ...task, status: 'PENDING', processedRows: 0, failureMessage: '头程服务查询失败', attemptCount: 2, nextRetryAt: '2026-09-03T10:34:28' }],
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.body.textContent).toContain('等待重试')
const retry = [...document.querySelectorAll<HTMLButtonElement>('button')]
.find((item) => item.textContent?.trim() === '立即重试')
retry?.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<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLButtonElement>('button')]
.some((item) => item.textContent?.trim() === '重试')).toBe(false)
;[...document.querySelectorAll<HTMLButtonElement>('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<HTMLButtonElement>('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 行')
})
})