2026-10-20 10:11:31 +08:00
|
|
|
import ElementPlus, { ElDialog } from 'element-plus'
|
2026-08-03 18:15:41 +08:00
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
|
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import {
|
|
|
|
|
downloadPlatformExportTask,
|
2026-08-04 09:05:19 +08:00
|
|
|
getPlatformExportTask,
|
2026-08-03 18:15:41 +08:00
|
|
|
listPlatformExportTasks,
|
|
|
|
|
retryPlatformExportTask,
|
|
|
|
|
type PlatformExportTask,
|
2026-08-03 18:49:18 +08:00
|
|
|
} from '@/modules/downloads/api/platformExportTasks'
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
import { useAuthStore } from '@/core/auth/auth'
|
2026-08-03 18:15:41 +08:00
|
|
|
import DownloadCenterView from './DownloadCenterView.vue'
|
2026-10-20 10:11:31 +08:00
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
|
|
|
import { ApiResponseError } from '@/shared/api/http'
|
2026-08-03 18:15:41 +08:00
|
|
|
|
2026-08-03 18:49:18 +08:00
|
|
|
vi.mock('@/modules/downloads/api/platformExportTasks', () => ({
|
2026-08-03 18:15:41 +08:00
|
|
|
downloadPlatformExportTask: vi.fn(),
|
2026-08-04 09:05:19 +08:00
|
|
|
getPlatformExportTask: vi.fn(),
|
2026-08-03 18:15:41 +08:00
|
|
|
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,
|
2026-09-03 10:39:55 +08:00
|
|
|
failureMessage: null, attemptCount: 1, maxAttempts: 3, nextRetryAt: null,
|
|
|
|
|
requestedBy: '1', requestedUsername: 'admin', requestedAt: '2026-08-03T10:00:00',
|
2026-08-03 18:15:41 +08:00
|
|
|
startedAt: '2026-08-03T10:00:01', completedAt: '2026-08-03T10:00:02', expiresAt: '2026-08-10T10:00:02',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enableAutoUnmount(afterEach)
|
2026-08-03 18:49:18 +08:00
|
|
|
afterEach(() => vi.useRealTimers())
|
2026-08-03 18:15:41 +08:00
|
|
|
|
|
|
|
|
describe('DownloadCenterView', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
2026-08-04 09:05:19 +08:00
|
|
|
document.body.innerHTML = ''
|
2026-08-03 18:15:41 +08:00
|
|
|
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']))
|
2026-08-04 09:05:19 +08:00
|
|
|
vi.mocked(getPlatformExportTask).mockResolvedValue(task)
|
2026-08-03 18:15:41 +08:00
|
|
|
vi.mocked(retryPlatformExportTask).mockResolvedValue({ ...task, status: 'PENDING' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-10-20 10:11:31 +08:00
|
|
|
const dialogButton = (label: string) => {
|
|
|
|
|
const found = Array.from(document.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')).find(button => button.textContent?.trim() === label)
|
|
|
|
|
expect(found, label).toBeDefined(); return found!
|
|
|
|
|
}
|
|
|
|
|
const factValue = (label: string) => {
|
|
|
|
|
const term = Array.from(document.querySelectorAll('[aria-label="下载任务资料"] dt')).find(node => node.textContent === label)
|
|
|
|
|
expect(term, label).toBeDefined(); return term!.nextElementSibling!.textContent
|
|
|
|
|
}
|
|
|
|
|
async function openTask(value: PlatformExportTask = task) {
|
|
|
|
|
vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [value], total: 1, page: 1, size: 20, pages: 1 })
|
|
|
|
|
const wrapper = mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [ElementPlus] } }); await flushPromises()
|
|
|
|
|
const open = () => Array.from(document.querySelectorAll<HTMLButtonElement>('.el-table__body .table-actions button')).find(button => button.textContent?.trim() === '详情')!.click()
|
|
|
|
|
open(); await flushPromises(); return { wrapper, open }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 18:15:41 +08:00
|
|
|
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('客户列表')
|
2026-08-12 10:59:49 +08:00
|
|
|
expect(document.querySelector('.data-table')?.textContent).not.toContain('失败原因')
|
2026-08-17 19:52:04 +08:00
|
|
|
expect(document.querySelector('.data-table')?.textContent).toContain('格式')
|
|
|
|
|
expect(document.querySelector('.data-table')?.textContent).toContain('完成时间')
|
|
|
|
|
expect(document.querySelector('.data-table')?.textContent).toContain('过期时间')
|
2026-08-12 10:59:49 +08:00
|
|
|
expect(document.querySelector('.failure-cell')).toBeNull()
|
2026-08-03 18:15:41 +08:00
|
|
|
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '下载')
|
|
|
|
|
button?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(downloadPlatformExportTask).toHaveBeenCalledWith('9')
|
|
|
|
|
expect(URL.createObjectURL).toHaveBeenCalled()
|
2026-08-04 09:05:19 +08:00
|
|
|
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')
|
2026-08-03 18:15:41 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-03 10:39:55 +08:00
|
|
|
it('shows retry for failed tasks with the source export permission', async () => {
|
2026-08-03 18:15:41 +08:00
|
|
|
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()
|
|
|
|
|
|
2026-08-12 10:59:49 +08:00
|
|
|
expect(document.querySelector('.failure-cell')).toBeNull()
|
2026-08-03 18:15:41 +08:00
|
|
|
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
|
|
|
|
|
button?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(retryPlatformExportTask).toHaveBeenCalledWith('9')
|
|
|
|
|
})
|
2026-08-03 18:49:18 +08:00
|
|
|
|
2026-09-03 10:39:55 +08:00
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-17 17:49:10 +08:00
|
|
|
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')
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-04 09:05:19 +08:00
|
|
|
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')
|
2026-08-13 11:57:44 +08:00
|
|
|
expect(document.querySelector('.el-drawer')).toBeNull()
|
2026-09-15 09:58:15 +08:00
|
|
|
expect(document.querySelector('.el-dialog.admin-density-dialog')).not.toBeNull()
|
|
|
|
|
expect(document.querySelectorAll('.task-detail-grid.density-fields > div')).toHaveLength(11)
|
2026-08-13 11:57:44 +08:00
|
|
|
const detailDownload = [...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
|
|
|
.find((item) => item.textContent?.trim() === '下载文件')
|
|
|
|
|
detailDownload?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(downloadPlatformExportTask).toHaveBeenCalledWith('9')
|
2026-08-04 09:05:19 +08:00
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-12 10:59:49 +08:00
|
|
|
it('keeps failure details out of the list and does not expose retry without the source export permission', async () => {
|
2026-08-04 09:05:19 +08:00
|
|
|
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()
|
2026-08-12 10:59:49 +08:00
|
|
|
vi.mocked(getPlatformExportTask).mockResolvedValue({ ...task, status: 'FAILED', failureMessage: 'IAM timeout' })
|
2026-08-04 09:05:19 +08:00
|
|
|
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
2026-08-12 10:59:49 +08:00
|
|
|
expect(document.body.textContent).not.toContain('IAM timeout')
|
2026-08-04 09:05:19 +08:00
|
|
|
expect([...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
|
|
|
.some((item) => item.textContent?.trim() === '重试')).toBe(false)
|
2026-08-12 10:59:49 +08:00
|
|
|
|
|
|
|
|
;[...document.querySelectorAll<HTMLButtonElement>('button')]
|
|
|
|
|
.find((item) => item.textContent?.trim() === '详情')?.click()
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(document.body.textContent).toContain('IAM timeout')
|
2026-08-04 09:05:19 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2026-09-30 14:01:58 +08:00
|
|
|
expect(wrapper.find('.el-table').attributes('aria-busy')).toBe('true')
|
2026-08-04 09:05:19 +08:00
|
|
|
resolveRequest({ records: [], page: 1, size: 20, total: 0, pages: 0 })
|
|
|
|
|
await flushPromises()
|
2026-09-30 14:01:58 +08:00
|
|
|
expect(wrapper.find('.el-table').attributes('aria-busy')).toBe('false')
|
2026-08-04 09:05:19 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-03 18:49:18 +08:00
|
|
|
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)
|
|
|
|
|
})
|
2026-08-04 09:05:19 +08:00
|
|
|
|
|
|
|
|
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 行')
|
|
|
|
|
})
|
2026-10-20 10:11:31 +08:00
|
|
|
|
|
|
|
|
it('keeps long facts and zero values complete through fullscreen and never uses a requester ID fallback', async () => {
|
|
|
|
|
const value: PlatformExportTask = { ...task, id: '9223372036854775807', requestedBy: '9223372036854775806', requestedUsername: null, taskNo: 'EXP-FULL-001', taskName: '完整导出任务名称'.repeat(25), fileName: '完整导出文件名称'.repeat(25) + '.xlsx', processedRows: 0, totalRows: 0, sizeBytes: 0 }
|
|
|
|
|
vi.mocked(getPlatformExportTask).mockResolvedValue(value)
|
|
|
|
|
const { wrapper } = await openTask(value)
|
|
|
|
|
const dialog = wrapper.findComponent(AppDialog)
|
|
|
|
|
expect(dialog.props()).toMatchObject({ title: '任务详情 · EXP-FULL-001', width: 'min(720px, calc(100vw - 28px))', fullscreenable: true, showConfirm: false })
|
|
|
|
|
expect(document.querySelector('[aria-label="下载任务资料"]')?.textContent).toContain(value.taskName)
|
|
|
|
|
expect(factValue('文件名称')).toBe(value.fileName)
|
|
|
|
|
expect(factValue('申请人')).toBe('—')
|
|
|
|
|
expect(factValue('处理数据')).toBe('0 / 0 行')
|
|
|
|
|
expect(factValue('文件大小')).toBe('0 B')
|
|
|
|
|
expect(document.body.textContent).not.toContain(value.requestedBy)
|
|
|
|
|
expect(document.body.textContent).not.toContain(value.id)
|
|
|
|
|
document.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click(); await flushPromises()
|
|
|
|
|
expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true)
|
|
|
|
|
expect(factValue('文件名称')).toBe(value.fileName)
|
|
|
|
|
dialogButton('下载文件').click(); await flushPromises()
|
|
|
|
|
expect(downloadPlatformExportTask).toHaveBeenCalledWith(value.id)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['RUNNING', '处理中', false], ['PENDING', '等待重试', true], ['EXPIRED', '已过期', false],
|
|
|
|
|
] as const)('shows %s facts and original retry/download eligibility', async (status,label,retry) => {
|
|
|
|
|
const value: PlatformExportTask = { ...task, status, attemptCount: 2, processedRows: 3, totalRows: null, sizeBytes: null, fileName: null, nextRetryAt: '2026-10-20T12:00:00' }
|
|
|
|
|
vi.mocked(getPlatformExportTask).mockResolvedValue(value)
|
|
|
|
|
await openTask(value)
|
|
|
|
|
expect(document.querySelector('.task-summary-heading')?.textContent).toContain(label)
|
|
|
|
|
expect(factValue('处理数据')).toBe('3 行'); expect(factValue('文件大小')).toBe('—'); expect(factValue('文件名称')).toBe('—')
|
|
|
|
|
expect(dialogButton('下载文件').disabled).toBe(true)
|
|
|
|
|
if (retry) {
|
|
|
|
|
expect(factValue('下次重试')).not.toBe('—')
|
|
|
|
|
dialogButton('立即重试').click(); await flushPromises()
|
|
|
|
|
expect(retryPlatformExportTask).toHaveBeenCalledWith(value.id)
|
|
|
|
|
} else {
|
|
|
|
|
expect(document.querySelector('.el-dialog__footer')?.textContent).not.toContain('重试')
|
|
|
|
|
expect(factValue('下次重试')).toBe('—')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('preserves the full failure reason when retry fails and refreshes the same task after retry succeeds', async () => {
|
|
|
|
|
const failed: PlatformExportTask = { ...task, status: 'FAILED', failureMessage: '完整失败原因'.repeat(90) }
|
|
|
|
|
vi.mocked(getPlatformExportTask).mockResolvedValue(failed)
|
|
|
|
|
vi.mocked(retryPlatformExportTask).mockRejectedValueOnce(new Error('重试失败')).mockResolvedValueOnce({ ...failed, status: 'PENDING' })
|
|
|
|
|
await openTask(failed)
|
|
|
|
|
expect(document.querySelector('.task-failure')?.textContent).toContain(failed.failureMessage)
|
|
|
|
|
dialogButton('重试').click(); await flushPromises()
|
|
|
|
|
expect(document.querySelector('.task-failure')?.textContent).toContain(failed.failureMessage)
|
|
|
|
|
expect(dialogButton('重试').disabled).toBe(false)
|
|
|
|
|
vi.mocked(listPlatformExportTasks).mockResolvedValue({ records: [{ ...failed, status: 'PENDING', attemptCount: 0, failureMessage: null }], total: 1, page: 1, size: 20, pages: 1 })
|
|
|
|
|
dialogButton('重试').click(); await flushPromises()
|
|
|
|
|
expect(retryPlatformExportTask).toHaveBeenCalledTimes(2)
|
|
|
|
|
expect(document.querySelector('.task-summary-heading')?.textContent).toContain('等待处理')
|
|
|
|
|
expect(document.querySelector('.task-failure')).toBeNull()
|
|
|
|
|
expect(dialogButton('下载文件').disabled).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows a compact detail error with diagnostics and restores actions only after reloading', async () => {
|
|
|
|
|
vi.mocked(getPlatformExportTask).mockRejectedValueOnce(new ApiResponseError('详情读取失败', 'A0500', 500, null, 'req-download-detail')).mockResolvedValueOnce(task)
|
|
|
|
|
await openTask()
|
|
|
|
|
expect(document.querySelector('.el-dialog .load-error-state')?.textContent).toContain('req-download-detail')
|
|
|
|
|
expect(document.querySelector('.el-dialog .load-error-state')?.classList.contains('is-compact')).toBe(true)
|
|
|
|
|
expect(dialogButton('下载文件').disabled).toBe(true)
|
|
|
|
|
const reload = Array.from(document.querySelectorAll<HTMLButtonElement>('.el-dialog button')).find(button => button.textContent?.trim() === '重新加载')!
|
|
|
|
|
reload.click(); await flushPromises()
|
|
|
|
|
expect(getPlatformExportTask).toHaveBeenCalledTimes(2)
|
|
|
|
|
expect(document.querySelector('.el-dialog .load-error-state')).toBeNull()
|
|
|
|
|
expect(dialogButton('下载文件').disabled).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps actions disabled while detail is loading and ignores an old response after close and reopen', async () => {
|
|
|
|
|
let release!: (value: PlatformExportTask) => void
|
|
|
|
|
vi.mocked(getPlatformExportTask).mockImplementationOnce(() => new Promise(resolve => { release = resolve })).mockResolvedValueOnce({ ...task, taskNo: 'EXP-NEW', taskName: '最新任务详情' })
|
|
|
|
|
const { wrapper, open } = await openTask()
|
|
|
|
|
expect(wrapper.findComponent(AppDialog).props('loading')).toBe(true)
|
|
|
|
|
expect(dialogButton('下载文件').disabled).toBe(true)
|
|
|
|
|
dialogButton('关闭').click(); await flushPromises()
|
|
|
|
|
wrapper.findComponent(AppDialog).vm.$emit('closed'); await flushPromises()
|
|
|
|
|
open(); await flushPromises()
|
|
|
|
|
expect(wrapper.findComponent(AppDialog).props('title')).toBe('任务详情 · EXP-NEW')
|
|
|
|
|
release({ ...task, taskName: '过期响应' }); await flushPromises()
|
|
|
|
|
expect(document.querySelector('.task-summary-heading')?.textContent).toContain('最新任务详情')
|
|
|
|
|
expect(document.querySelector('.task-summary-heading')?.textContent).not.toContain('过期响应')
|
|
|
|
|
expect(wrapper.findComponent(AppDialog).props('loading')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows a missing business number as — without exposing the internal task ID', async () => {
|
|
|
|
|
const value: PlatformExportTask = { ...task, id: '9223372036854775807', taskNo: '' }
|
|
|
|
|
vi.mocked(getPlatformExportTask).mockResolvedValue(value)
|
|
|
|
|
const { wrapper } = await openTask(value)
|
|
|
|
|
expect(wrapper.findComponent(AppDialog).props('title')).toBe('任务详情')
|
|
|
|
|
expect(factValue('任务编号')).toBe('—')
|
|
|
|
|
expect(document.querySelector('.el-dialog')?.textContent).not.toContain(value.id)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-03 18:15:41 +08:00
|
|
|
})
|