fix(users): discard stale user attribute responses

This commit is contained in:
chihao.ou
2026-09-22 21:49:11 +08:00
parent b0e32fe5be
commit 1f5cfe8973
2 changed files with 48 additions and 4 deletions
@@ -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('')
})
})