fix(usage): ignore obsolete cleanup task list responses

This commit is contained in:
chihao.ou
2026-09-30 02:45:47 +08:00
parent a60a29549f
commit 8a495b0a54
2 changed files with 68 additions and 1 deletions
@@ -155,6 +155,7 @@ const cancelConfirmVisible = ref(false)
const canceling = ref(false)
const cancelTarget = ref<UsageCleanupTask | null>(null)
let pollTimer: number | null = null
let taskRequestId = 0
const noop = () => {}
@@ -183,6 +184,7 @@ const stopPolling = () => {
}
const handleClose = () => {
taskRequestId++
stopPolling()
confirmVisible.value = false
cancelConfirmVisible.value = false
@@ -237,12 +239,14 @@ const getUserTimezone = () => {
const loadTasks = async () => {
if (!props.show) return
const requestId = ++taskRequestId
tasksLoading.value = true
try {
const res = await adminUsageAPI.listCleanupTasks({
page: tasksPage.value,
page_size: tasksPageSize.value
})
if (requestId !== taskRequestId) return
tasks.value = res.items || []
tasksTotal.value = res.total || 0
if (res.page) {
@@ -252,10 +256,11 @@ const loadTasks = async () => {
tasksPageSize.value = res.page_size
}
} catch (error) {
if (requestId !== taskRequestId) return
console.error('Failed to load cleanup tasks:', error)
appStore.showError(t('admin.usage.cleanup.loadFailed'))
} finally {
tasksLoading.value = false
if (requestId === taskRequestId) tasksLoading.value = false
}
}
@@ -376,12 +381,14 @@ watch(
loadTasks()
startPolling()
} else {
taskRequestId++
stopPolling()
}
}
)
onUnmounted(() => {
taskRequestId++
stopPolling()
})
</script>
@@ -0,0 +1,60 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import UsageCleanupDialog from '../UsageCleanupDialog.vue'
import Pagination from '@/components/common/Pagination.vue'
const { listCleanupTasks, showError } = vi.hoisted(() => ({ listCleanupTasks: vi.fn(), showError: vi.fn() }))
vi.mock('@/components/admin/usage/UsageFilters.vue', () => ({ default: { template: '<div />' } }))
vi.mock('@/api/admin/usage', () => ({ adminUsageAPI: { listCleanupTasks } }))
vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError }) }))
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
enableAutoUnmount(afterEach)
beforeEach(() => { vi.clearAllMocks(); vi.spyOn(console, 'error').mockImplementation(() => {}) })
afterEach(() => { vi.restoreAllMocks() })
const page = (number: number) => ({ items: [{ id: number, status: 'succeeded', deleted_rows: number, filters: {} }], total: 15, page: number, page_size: 5 })
async function openDialog() {
listCleanupTasks.mockResolvedValueOnce(page(1))
const wrapper = mount(UsageCleanupDialog, {
props: { show: false, filters: {}, startDate: '2026-09-01', endDate: '2026-09-02' },
global: { stubs: { BaseDialog: { template: '<div><slot/><slot name="footer"/></div>' }, UsageFilters: true, ConfirmDialog: true, Pagination: true } },
})
await wrapper.setProps({ show: true })
await flushPromises()
return wrapper
}
describe('cleanup task pagination', () => {
it.each(['success', 'failure'])('ignores an older page request that completes with %s', async (outcome) => {
const wrapper = await openDialog()
let resolve!: (value: object) => void
let reject!: (error: Error) => void
listCleanupTasks.mockImplementationOnce(() => new Promise((res, rej) => { resolve = res; reject = rej }))
wrapper.findComponent(Pagination).vm.$emit('update:page', 2)
await flushPromises()
listCleanupTasks.mockResolvedValueOnce(page(3))
wrapper.findComponent(Pagination).vm.$emit('update:page', 3)
await flushPromises()
if (outcome === 'success') resolve(page(2))
else reject(new Error('old failure'))
await flushPromises()
expect(wrapper.findComponent(Pagination).props('page')).toBe(3)
expect(wrapper.text()).toContain('#3')
expect(showError).not.toHaveBeenCalled()
})
it('ignores a pending result after the dialog is closed and reopened', async () => {
const wrapper = await openDialog()
let resolve!: (value: object) => void
listCleanupTasks.mockImplementationOnce(() => new Promise(res => { resolve = res }))
wrapper.findComponent(Pagination).vm.$emit('update:page', 2)
await flushPromises()
await wrapper.setProps({ show: false })
listCleanupTasks.mockResolvedValueOnce(page(1))
await wrapper.setProps({ show: true })
await flushPromises()
resolve(page(2))
await flushPromises()
expect(wrapper.findComponent(Pagination).props('page')).toBe(1)
expect(wrapper.text()).toContain('#1')
})
})