mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 15:43:13 +08:00
fix(usage): ignore obsolete cleanup task list responses
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user