mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 00:18:55 +00:00
fix(users): ignore superseded balance history requests
This commit is contained in:
@@ -172,7 +172,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI, type BalanceHistoryItem } from '@/api/admin'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
@@ -192,6 +192,9 @@ const total = ref(0)
|
||||
const totalRecharged = ref(0)
|
||||
const pageSize = 15
|
||||
const typeFilter = ref('')
|
||||
let requestVersion = 0
|
||||
|
||||
onUnmounted(() => { requestVersion++ })
|
||||
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize) || 1)
|
||||
|
||||
@@ -208,6 +211,7 @@ const typeOptions = computed(() => [
|
||||
|
||||
// Watch modal open
|
||||
watch(() => props.show, (v) => {
|
||||
requestVersion++
|
||||
if (v && props.user) {
|
||||
typeFilter.value = ''
|
||||
loadHistory(1)
|
||||
@@ -216,6 +220,7 @@ watch(() => props.show, (v) => {
|
||||
|
||||
const loadHistory = async (page: number) => {
|
||||
if (!props.user) return
|
||||
const version = ++requestVersion
|
||||
loading.value = true
|
||||
currentPage.value = page
|
||||
try {
|
||||
@@ -225,13 +230,15 @@ const loadHistory = async (page: number) => {
|
||||
pageSize,
|
||||
typeFilter.value || undefined
|
||||
)
|
||||
if (version !== requestVersion) return
|
||||
history.value = res.items || []
|
||||
total.value = res.total || 0
|
||||
totalRecharged.value = res.total_recharged || 0
|
||||
} catch (error) {
|
||||
if (version !== requestVersion) return
|
||||
console.error('Failed to load balance history:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (version === requestVersion) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import type { AdminUser } from '@/types'
|
||||
import UserBalanceHistoryModal from '../UserBalanceHistoryModal.vue'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ getUserBalanceHistory: vi.fn() }))
|
||||
vi.mock('@/api/admin', () => ({ adminAPI: { users: mocks } }))
|
||||
vi.mock('@/utils/format', () => ({ formatDateTime: () => 'date' }))
|
||||
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
|
||||
enableAutoUnmount(afterEach)
|
||||
beforeEach(() => { vi.clearAllMocks(); vi.spyOn(console, 'error').mockImplementation(() => {}) })
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
function result(id: number) {
|
||||
return { items: [{ id, type: 'admin_balance', value: id, notes: `History ${id}` }], total: 1, total_recharged: id }
|
||||
}
|
||||
function deferred() {
|
||||
let resolve!: (value: ReturnType<typeof result>) => void
|
||||
let reject!: (reason: unknown) => void
|
||||
const promise = new Promise<ReturnType<typeof result>>((res, rej) => { resolve = res; reject = rej })
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
async function openDialog() {
|
||||
const wrapper = mount(UserBalanceHistoryModal, {
|
||||
props: { show: false, user: { id: 1, email: 'one@example.com', balance: 1 } as AdminUser },
|
||||
global: { stubs: { BaseDialog: { props: ['show'], template: '<div v-if="show"><slot /></div>' }, Icon: true, Select: true } }
|
||||
})
|
||||
await wrapper.setProps({ show: true })
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('UserBalanceHistoryModal request ordering', () => {
|
||||
it('keeps the new user history when an old response finishes later', async () => {
|
||||
const old = deferred()
|
||||
mocks.getUserBalanceHistory.mockReturnValueOnce(old.promise).mockResolvedValueOnce(result(20))
|
||||
const wrapper = await openDialog()
|
||||
await wrapper.setProps({ show: false })
|
||||
await wrapper.setProps({ show: true, user: { id: 2, email: 'two@example.com', balance: 2 } as AdminUser })
|
||||
await flushPromises()
|
||||
old.resolve(result(10))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('History 20')
|
||||
expect(wrapper.text()).not.toContain('History 10')
|
||||
expect(wrapper.text()).toContain('$20.00')
|
||||
})
|
||||
|
||||
it('does not end the current filter loading state when an old request finishes', async () => {
|
||||
const old = deferred()
|
||||
const current = deferred()
|
||||
mocks.getUserBalanceHistory.mockReturnValueOnce(old.promise).mockReturnValueOnce(current.promise)
|
||||
const wrapper = await openDialog()
|
||||
wrapper.findComponent({ name: 'Select' }).vm.$emit('change', 'balance')
|
||||
await flushPromises()
|
||||
old.resolve(result(10))
|
||||
await flushPromises()
|
||||
expect(wrapper.find('svg.animate-spin').exists()).toBe(true)
|
||||
expect(wrapper.text()).not.toContain('History 10')
|
||||
current.resolve(result(20))
|
||||
await flushPromises()
|
||||
expect(wrapper.find('svg.animate-spin').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('History 20')
|
||||
})
|
||||
|
||||
it.each(['close', 'unmount'])('ignores failures after %s', async (action) => {
|
||||
const pending = deferred()
|
||||
mocks.getUserBalanceHistory.mockReturnValueOnce(pending.promise)
|
||||
const wrapper = await openDialog()
|
||||
if (action === 'close') await wrapper.setProps({ show: false })
|
||||
else wrapper.unmount()
|
||||
pending.reject(new Error('Stale error'))
|
||||
await flushPromises()
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still reports current failures and ends loading', async () => {
|
||||
const error = new Error('Current error')
|
||||
mocks.getUserBalanceHistory.mockRejectedValueOnce(error)
|
||||
const wrapper = await openDialog()
|
||||
await flushPromises()
|
||||
expect(console.error).toHaveBeenCalledWith('Failed to load balance history:', error)
|
||||
expect(wrapper.find('svg.animate-spin').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user