From 3f2f66f3280d5c6ca457eb57a01adac83b67052d Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Wed, 7 Oct 2026 14:54:14 +0800 Subject: [PATCH] fix(profile): preserve username edits made during save --- .../user/profile/ProfileEditForm.vue | 9 +++- .../__tests__/ProfileEditForm.draft.spec.ts | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/user/profile/__tests__/ProfileEditForm.draft.spec.ts diff --git a/frontend/src/components/user/profile/ProfileEditForm.vue b/frontend/src/components/user/profile/ProfileEditForm.vue index b3605e0ede..c9ea6db1e4 100644 --- a/frontend/src/components/user/profile/ProfileEditForm.vue +++ b/frontend/src/components/user/profile/ProfileEditForm.vue @@ -58,10 +58,12 @@ const authStore = useAuthStore() const appStore = useAppStore() const username = ref(props.initialUsername) +let savedUsername = props.initialUsername const loading = ref(false) watch(() => props.initialUsername, (val) => { - username.value = val + if (username.value === savedUsername) username.value = val + savedUsername = val }) const handleUpdateProfile = async () => { @@ -71,10 +73,13 @@ const handleUpdateProfile = async () => { } loading.value = true + const submittedUsername = username.value try { const updatedUser = await userAPI.updateProfile({ - username: username.value + username: submittedUsername }) + if (username.value === submittedUsername) username.value = updatedUser.username + savedUsername = updatedUser.username authStore.user = updatedUser appStore.showSuccess(t('profile.updateSuccess')) } catch (error: unknown) { diff --git a/frontend/src/components/user/profile/__tests__/ProfileEditForm.draft.spec.ts b/frontend/src/components/user/profile/__tests__/ProfileEditForm.draft.spec.ts new file mode 100644 index 0000000000..6cc08bf7e0 --- /dev/null +++ b/frontend/src/components/user/profile/__tests__/ProfileEditForm.draft.spec.ts @@ -0,0 +1,53 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { defineComponent, reactive } from 'vue' +import ProfileEditForm from '../ProfileEditForm.vue' + +const mocks = vi.hoisted(() => ({ updateProfile: vi.fn(), state: { user: { username: 'alice' } } })) +vi.mock('@/api', () => ({ userAPI: { updateProfile: mocks.updateProfile } })) +vi.mock('@/stores/auth', () => ({ useAuthStore: () => reactive(mocks.state) })) +vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn(), showSuccess: vi.fn() }) })) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) +enableAutoUnmount(afterEach) +beforeEach(() => { + vi.resetAllMocks() + mocks.state.user = { username: 'alice' } +}) + +const Harness = defineComponent({ + components: { ProfileEditForm }, + setup: () => ({ auth: reactive(mocks.state) }), + template: '' +}) + +describe('profile username draft', () => { + it.each(['next-draft', 'alice'])('preserves %s typed while the previous username is being saved', async (draft) => { + let finish!: (value: unknown) => void + mocks.updateProfile.mockReturnValue(new Promise(resolve => { finish = resolve })) + const wrapper = mount(Harness) + await wrapper.get('#username').setValue('submitted') + await wrapper.get('form').trigger('submit') + expect(mocks.updateProfile).toHaveBeenCalledWith({ username: 'submitted' }) + await wrapper.get('#username').setValue(draft) + finish({ username: 'submitted' }) + await flushPromises() + expect(mocks.state.user.username).toBe('submitted') + expect((wrapper.get('#username').element as HTMLInputElement).value).toBe(draft) + }) + + it('still synchronizes a pristine input with profile updates', async () => { + const wrapper = mount(Harness) + reactive(mocks.state).user = { username: 'updated' } + await flushPromises() + expect((wrapper.get('#username').element as HTMLInputElement).value).toBe('updated') + }) + + it('shows the server username after saving without further edits', async () => { + mocks.updateProfile.mockResolvedValue({ username: 'saved-name' }) + const wrapper = mount(Harness) + await wrapper.get('#username').setValue(' saved-name ') + await wrapper.get('form').trigger('submit') + await flushPromises() + expect((wrapper.get('#username').element as HTMLInputElement).value).toBe('saved-name') + }) +})