mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(users): discard stale user attribute responses
This commit is contained in:
@@ -112,6 +112,7 @@ const emit = defineEmits<Emits>()
|
||||
const loading = ref(false)
|
||||
const attributes = ref<UserAttributeDefinition[]>([])
|
||||
const localValues = ref<UserAttributeValuesMap>({})
|
||||
let requestVersion = 0
|
||||
|
||||
const loadAttributes = async () => {
|
||||
loading.value = true
|
||||
@@ -126,9 +127,11 @@ const loadAttributes = async () => {
|
||||
|
||||
const loadUserValues = async () => {
|
||||
if (!props.userId) return
|
||||
const version = ++requestVersion
|
||||
|
||||
try {
|
||||
const values = await adminAPI.userAttributes.getUserAttributeValues(props.userId)
|
||||
if (version !== requestVersion) return
|
||||
const valuesMap: UserAttributeValuesMap = {}
|
||||
values.forEach(v => {
|
||||
valuesMap[v.attribute_id] = v.value
|
||||
@@ -184,12 +187,11 @@ watch(() => props.modelValue, (newVal) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => props.userId, (newUserId) => {
|
||||
watch(() => props.userId, (newUserId, _, onCleanup) => {
|
||||
onCleanup(() => { requestVersion++ })
|
||||
localValues.value = {}
|
||||
if (newUserId) {
|
||||
loadUserValues()
|
||||
} else {
|
||||
// Reset for new user
|
||||
localValues.value = {}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import UserAttributeForm from '../UserAttributeForm.vue'
|
||||
const mocks = vi.hoisted(() => ({ getUserAttributeValues: vi.fn(), listEnabledDefinitions: vi.fn() }))
|
||||
vi.mock('@/api/admin', () => ({ adminAPI: { userAttributes: mocks } }))
|
||||
enableAutoUnmount(afterEach)
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.listEnabledDefinitions.mockResolvedValue([{ id: 1, name: 'Team', type: 'text' }])
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
function deferred() {
|
||||
let resolve!: (value: unknown) => void
|
||||
const promise = new Promise(res => { resolve = res })
|
||||
return { promise, resolve }
|
||||
}
|
||||
function open() {
|
||||
return mount(UserAttributeForm, { props: { userId: 1, modelValue: {} }, global: { stubs: { Select: true } } })
|
||||
}
|
||||
describe('user attribute request ownership', () => {
|
||||
it('does not emit previous user values after a newer user has loaded', async () => {
|
||||
const old = deferred()
|
||||
mocks.getUserAttributeValues.mockReturnValueOnce(old.promise).mockResolvedValueOnce([{ attribute_id: 1, value: 'current' }])
|
||||
const w = open(); await w.setProps({ userId: 2 }); await flushPromises()
|
||||
old.resolve([{ attribute_id: 1, value: 'old' }]); await flushPromises()
|
||||
expect(w.get('input').element.value).toBe('current')
|
||||
expect(w.emitted('update:modelValue')).toEqual([[{ 1: 'current' }]])
|
||||
})
|
||||
it('does not restore values after resetting to a new user', async () => {
|
||||
const old = deferred(); mocks.getUserAttributeValues.mockReturnValueOnce(old.promise)
|
||||
const w = open(); await flushPromises(); await w.setProps({ userId: undefined })
|
||||
old.resolve([{ attribute_id: 1, value: 'old' }]); await flushPromises()
|
||||
expect(w.get('input').element.value).toBe('')
|
||||
expect(w.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
it('clears the previous values before a new user request fails', async () => {
|
||||
mocks.getUserAttributeValues.mockResolvedValueOnce([{ attribute_id: 1, value: 'old' }]).mockRejectedValueOnce(new Error('unavailable'))
|
||||
const w = open(); await flushPromises(); await w.setProps({ userId: 2 }); await flushPromises()
|
||||
expect(w.get('input').element.value).toBe('')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user