Ver código fonte

✨ feat(profile): 新增个人资料编辑功能

- 在个人中心页面添加编辑资料弹窗,支持修改头像和昵称
- 新增头像上传组件,支持图片选择和上传,添加文件大小限制
- 扩展认证hook,新增refreshUser方法用于刷新用户数据
- 优化头像上传体验,移除冗余的成功/失败提示,由调用方统一处理

🐛 fix(components): 修复输入框事件兼容性问题

- 修复商品规格选择器输入框事件处理,兼容Taro小程序和React事件格式
- 修复商品详情页数量输入框事件处理,确保跨平台兼容性

✅ test(profile): 添加个人资料编辑功能单元测试

- 新增编辑资料功能完整单元测试,覆盖头像上传、昵称修改等场景
- 验证弹窗显示、数据绑定、API调用和错误处理逻辑
- 确保组件交互和状态管理正确性

🔧 chore(settings): 扩展Claude工具权限

- 在Claude配置中添加常用系统命令权限,包括网络诊断和文件操作命令
yourname 4 semanas atrás
pai
commit
5ce6e6969b

+ 6 - 1
.claude/settings.local.json

@@ -74,7 +74,12 @@
       "Bash(npx tsx:*)",
       "Bash(pnpm run build:*)",
       "Bash(VITEST=true pnpm test:*)",
-      "Bash(env:*)"
+      "Bash(env:*)",
+      "Bash(nslookup:*)",
+      "Bash(host:*)",
+      "Bash(netstat:*)",
+      "Bash(ss:*)",
+      "Bash(ls:*)"
     ],
     "deny": [],
     "ask": []

+ 5 - 1
mini/src/components/goods-spec-selector/index.tsx

@@ -371,7 +371,11 @@ export function GoodsSpecSelector({
                 className="quantity-input"
                 type="number"
                 value={quantity === 0 ? '' : quantity.toString()}
-                onInput={(e) => handleQuantityChange(e.detail.value)}
+                onInput={(e) => {
+                  // 兼容Taro小程序事件(event.detail.value)和React事件(event.target.value)
+                  const value = e.detail?.value || (e.target as any)?.value || ''
+                  handleQuantityChange(value)
+                }}
                 onBlur={handleQuantityBlur}
                 placeholder="1"
                 maxlength={3}

+ 4 - 12
mini/src/components/ui/avatar-upload.tsx

@@ -1,8 +1,7 @@
 import { useState } from 'react'
-import { View, Text, Image } from '@tarojs/components'
+import { View, Image } from '@tarojs/components'
 import Taro from '@tarojs/taro'
 import { cn } from '@/utils/cn'
-import { Button } from '@/components/ui/button'
 import { uploadFromSelect, type UploadResult } from '@/utils/minio'
 
 interface AvatarUploadProps {
@@ -34,7 +33,9 @@ export function AvatarUpload({
         'avatars',
         {
           sourceType: ['album', 'camera'],
-          count: 1
+          count: 1,
+          accept: 'image/*',
+          maxSize: 2 * 1024 * 1024 // 限制为2MB,避免触发分段上传
         },
         {
           onProgress: (event) => {
@@ -47,18 +48,10 @@ export function AvatarUpload({
           },
           onComplete: () => {
             Taro.hideLoading()
-            Taro.showToast({
-              title: '上传成功',
-              icon: 'success'
-            })
           },
           onError: (error) => {
             Taro.hideLoading()
             onUploadError?.(error)
-            Taro.showToast({
-              title: '上传失败',
-              icon: 'none'
-            })
           }
         }
       )
@@ -74,7 +67,6 @@ export function AvatarUpload({
   }
 
   const avatarSize = size
-  const iconSize = Math.floor(size / 4)
 
   return (
     <View 

+ 5 - 1
mini/src/pages/goods-detail/index.tsx

@@ -570,7 +570,11 @@ export default function GoodsDetailPage() {
               className="quantity-input"
               type="number"
               value={quantity === 0 ? '' : quantity.toString()}
-              onInput={(e) => handleQuantityChange(e.detail.value)}
+              onInput={(e) => {
+                // 兼容Taro小程序事件(event.detail.value)和React事件(event.target.value)
+                const value = e.detail?.value || (e.target as any)?.value || ''
+                handleQuantityChange(value)
+              }}
               onBlur={handleQuantityBlur}
               placeholder="1"
               maxlength={3}

+ 156 - 19
mini/src/pages/profile/index.tsx

@@ -6,18 +6,25 @@ import { TabBarLayout } from '@/layouts/tab-bar-layout'
 import { useAuth } from '@/utils/auth'
 import { Button } from '@/components/ui/button'
 import { Navbar } from '@/components/ui/navbar'
+import { AvatarUpload } from '@/components/ui/avatar-upload'
+import { Input } from '@/components/ui/input'
+import { type UploadResult } from '@/utils/minio'
 import TDesignUserCenterCard from '@/components/tdesign/user-center-card'
 import TDesignOrderGroup from '@/components/tdesign/order-group'
 import TDesignCellGroup from '@/components/tdesign/cell-group'
 import TDesignCell from '@/components/tdesign/cell'
 import TDesignPopup from '@/components/tdesign/popup'
 import TDesignIcon from '@/components/tdesign/icon'
-import { creditBalanceClient, authClient } from '@/api'
+import { creditBalanceClient } from '@/api'
 import './index.css'
 
 const ProfilePage: React.FC = () => {
-  const { user: userProfile, logout, isLoading: loading, updateUser } = useAuth()
+  const { user: userProfile, logout, isLoading: loading, updateUser, refreshUser } = useAuth()
   const [showCustomerService, setShowCustomerService] = useState(false)
+  const [showEditProfile, setShowEditProfile] = useState(false)
+  const [editingAvatar, setEditingAvatar] = useState<string | undefined>(undefined)
+  const [editingAvatarFileId, setEditingAvatarFileId] = useState<number | undefined>(undefined)
+  const [editingNickname, setEditingNickname] = useState('')
 
   // 查询用户信用额度
   const {
@@ -62,23 +69,9 @@ const ProfilePage: React.FC = () => {
   usePullDownRefresh(() => {
     console.debug('个人中心下拉刷新触发')
 
-    // 刷新用户数据和信用额度数据
-    const refreshUserData = async () => {
-      try {
-        const response = await authClient.me.$get({})
-        if (response.status === 200) {
-          const updatedUser = await response.json()
-          updateUser(updatedUser)
-          console.debug('用户数据刷新成功')
-        }
-      } catch (error) {
-        console.error('刷新用户数据失败:', error)
-      }
-    }
-
     // 同时刷新用户数据和信用额度数据
     Promise.all([
-      refreshUserData(),
+      refreshUser(),
       refetchCreditBalance()
     ]).finally(() => {
       console.debug('个人中心下拉刷新完成')
@@ -118,12 +111,98 @@ const ProfilePage: React.FC = () => {
 
 
   const handleEditProfile = () => {
+    setShowEditProfile(true)
+    setEditingAvatar(userProfile?.avatarFile?.fullUrl)
+    setEditingAvatarFileId(userProfile?.avatarFile?.id)
+    setEditingNickname(userProfile?.username || '')
+  }
+
+  const handleCloseEditProfile = () => {
+    setShowEditProfile(false)
+    setEditingAvatar(undefined)
+    setEditingAvatarFileId(undefined)
+    setEditingNickname('')
+  }
+
+  const handleAvatarUploadSuccess = async (result: UploadResult) => {
+    // result 包含上传后的文件信息,包括 fileId 和 fileUrl
+    if (result?.fileUrl && result?.fileId) {
+      setEditingAvatar(result.fileUrl)
+      setEditingAvatarFileId(result.fileId)
+
+      try {
+        // 立即更新用户头像到后端
+        await updateUser({ avatarFileId: result.fileId })
+        // 更新成功后不显示额外提示,updateUser内部已有成功提示
+
+        // 刷新用户数据,确保获取完整的avatarFile关系
+        try {
+          await refreshUser()
+        } catch (refreshError) {
+          console.error('刷新用户数据失败:', refreshError)
+          // 刷新失败不影响主流程
+        }
+      } catch (error) {
+        console.error('更新用户头像失败:', error)
+        // updateUser内部已有错误提示,这里不需要重复显示
+      }
+    }
+  }
+
+  const handleAvatarUploadError = (error: Error) => {
+    console.error('头像上传失败:', error)
+    const errorMessage = error.message || '头像上传失败'
     Taro.showToast({
-      title: '功能开发中...',
-      icon: 'none'
+      title: errorMessage.length > 20 ? errorMessage.substring(0, 20) + '...' : errorMessage,
+      icon: 'none',
+      duration: 3000
     })
   }
 
+  const handleSaveProfile = async () => {
+    if (!userProfile) return
+
+    try {
+      const updateData: any = {}
+      let hasChanges = false
+
+      // 检查昵称是否有变化
+      if (editingNickname !== userProfile.username) {
+        updateData.username = editingNickname
+        hasChanges = true
+      }
+
+      // 检查头像是否有变化
+      const currentAvatarFileId = userProfile.avatarFile?.id
+      if (editingAvatarFileId !== undefined && editingAvatarFileId !== currentAvatarFileId) {
+        updateData.avatarFileId = editingAvatarFileId
+        hasChanges = true
+      }
+
+      if (hasChanges) {
+        // 先更新用户信息到后端
+        await updateUser(updateData)
+
+        // 然后刷新用户数据,确保获取完整的avatarFile关系
+        try {
+          await refreshUser()
+        } catch (refreshError) {
+          console.error('刷新用户数据失败:', refreshError)
+          // 刷新失败不影响主流程,继续关闭弹窗
+        }
+
+        handleCloseEditProfile()
+      } else {
+        Taro.showToast({
+          title: '没有更改',
+          icon: 'none'
+        })
+      }
+    } catch (error) {
+      console.error('更新用户信息失败:', error)
+    }
+  }
+
 
   const handleCustomerService = () => {
     setShowCustomerService(true)
@@ -399,6 +478,64 @@ const ProfilePage: React.FC = () => {
           </View>
         </View>
       </TDesignPopup>
+
+      {/* 编辑资料弹窗 */}
+      <TDesignPopup
+        visible={showEditProfile}
+        placement="bottom"
+        onVisibleChange={(visible) => setShowEditProfile(visible)}
+        onClose={handleCloseEditProfile}
+      >
+        <View className="popup-content">
+          <View className="popup-title border-bottom-1px">
+            编辑资料
+          </View>
+
+          <View className="p-5 flex flex-col items-center">
+            {/* 头像上传 */}
+            <View className="mb-6">
+              <AvatarUpload
+                currentAvatar={editingAvatar}
+                onUploadSuccess={handleAvatarUploadSuccess}
+                onUploadError={handleAvatarUploadError}
+                size={120}
+                editable={true}
+              />
+              <Text className="text-center text-gray-500 text-sm mt-2">点击头像更换</Text>
+            </View>
+
+            {/* 昵称输入 */}
+            <View className="w-full mb-6">
+              <Text className="text-gray-700 text-sm mb-2">昵称</Text>
+              <Input
+                className="w-full p-3 border border-gray-300 rounded-lg"
+                value={editingNickname}
+                onChange={(value) => setEditingNickname(value)}
+                placeholder="请输入昵称"
+                maxlength={20}
+              />
+            </View>
+
+            {/* 按钮 */}
+            <View className="w-full flex flex-row gap-3">
+              <Button
+                variant="outline"
+                className="flex-1"
+                onClick={handleCloseEditProfile}
+              >
+                取消
+              </Button>
+              <Button
+                variant="default"
+                className="flex-1"
+                onClick={handleSaveProfile}
+              >
+                保存
+              </Button>
+            </View>
+          </View>
+        </View>
+      </TDesignPopup>
     </TabBarLayout>
   )
 }

+ 17 - 0
mini/src/utils/auth.tsx

@@ -17,6 +17,7 @@ interface AuthContextType {
   logout: () => Promise<void>
   register: (data: RegisterRequest) => Promise<User>
   updateUser: (userData: Partial<User>) => void
+  refreshUser: () => Promise<User | null>
   isLoading: boolean
   isLoggedIn: boolean
 }
@@ -223,12 +224,28 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
 
   const updateUser = updateUserMutation.mutateAsync
 
+  const refreshUser = async (): Promise<User | null> => {
+    try {
+      const freshUser = await fetchUserInfo()
+      if (freshUser) {
+        setUser(freshUser)
+      } else {
+        setUser(null)
+      }
+      return freshUser
+    } catch (error) {
+      console.error('刷新用户信息失败:', error)
+      return null
+    }
+  }
+
   const value = {
     user: user || null,
     login: loginMutation.mutateAsync,
     logout: logoutMutation.mutateAsync,
     register: registerMutation.mutateAsync,
     updateUser,
+    refreshUser,
     isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending || silentLoginMutation.isPending,
     isLoggedIn: !!user,
   }

+ 3 - 0
mini/tests/__mocks__/taroMock.ts

@@ -24,6 +24,7 @@ export const mockGetCurrentPages = jest.fn()
 export const mockGetNetworkType = jest.fn()
 export const mockRedirectTo = jest.fn()
 export const mockRequest = jest.fn()
+export const mockUsePullDownRefresh = jest.fn()
 
 // 存储相关
 export const mockGetStorageSync = jest.fn()
@@ -59,6 +60,7 @@ export default {
   redirectTo: mockRedirectTo,
   useRouter: () => mockUseRouter(),
   useLoad: (callback: any) => mockUseLoad(callback),
+  usePullDownRefresh: mockUsePullDownRefresh,
 
   // 微信相关
   openCustomerServiceChat: mockOpenCustomerServiceChat,
@@ -110,6 +112,7 @@ export {
   mockRedirectTo as redirectTo,
   mockUseRouter as useRouter,
   mockUseLoad as useLoad,
+  mockUsePullDownRefresh as usePullDownRefresh,
   mockOpenCustomerServiceChat as openCustomerServiceChat,
   mockRequestPayment as requestPayment,
   mockGetEnv as getEnv,

+ 436 - 0
mini/tests/unit/pages/profile/edit-profile.test.tsx

@@ -0,0 +1,436 @@
+/**
+ * 个人中心编辑资料功能单元测试
+ * 测试头像和昵称编辑功能
+ */
+
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import ProfilePage from '@/pages/profile/index'
+import { creditBalanceClient } from '@/api'
+import { useAuth } from '@/utils/auth'
+import Taro from '@tarojs/taro'
+
+// Mock API客户端
+jest.mock('@/api', () => ({
+  creditBalanceClient: {
+    me: {
+      $get: jest.fn(),
+    },
+  },
+  authClient: {
+    me: {
+      $get: jest.fn(),
+      $put: jest.fn(),
+    },
+  },
+}))
+
+// Mock 认证hook
+jest.mock('@/utils/auth', () => ({
+  useAuth: jest.fn(),
+}))
+
+// Mock TDesign组件 - 复用现有测试中的mock
+jest.mock('@/components/tdesign/user-center-card', () => ({
+  __esModule: true,
+  default: ({ avatar, nickname, isLoggedIn, onUserEdit, className }: any) => (
+    <div data-testid="user-center-card" className={className}>
+      <div data-testid="avatar">{avatar}</div>
+      <div data-testid="nickname">{nickname}</div>
+      <div data-testid="is-logged-in">{isLoggedIn ? '已登录' : '未登录'}</div>
+      <button data-testid="edit-button" onClick={onUserEdit}>编辑</button>
+    </div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/order-group', () => ({
+  __esModule: true,
+  default: ({ orderTagInfos, title, desc, onTopClick, onItemClick }: any) => (
+    <div data-testid="order-group">
+      <div data-testid="order-title">{title}</div>
+      <div data-testid="order-desc">{desc}</div>
+      <button data-testid="top-click" onClick={onTopClick}>查看全部</button>
+      {orderTagInfos.map((item: any, index: number) => (
+        <button key={index} data-testid={`order-item-${index}`} onClick={() => onItemClick(item)}>
+          {item.title}
+        </button>
+      ))}
+    </div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/cell-group', () => ({
+  __esModule: true,
+  default: ({ children }: any) => (
+    <div data-testid="cell-group">{children}</div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/cell', () => ({
+  __esModule: true,
+  default: ({ title, bordered, onClick, noteSlot }: any) => (
+    <div data-testid="cell" data-bordered={bordered}>
+      <div data-testid="cell-title">{title}</div>
+      <button data-testid="cell-click" onClick={onClick}>点击</button>
+      <div data-testid="cell-note">{noteSlot}</div>
+    </div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/popup', () => ({
+  __esModule: true,
+  default: ({ visible, placement, onClose, children }: any) => (
+    visible ? (
+      <div data-testid="popup" data-placement={placement}>
+        {children}
+        <button data-testid="popup-close" onClick={onClose}>关闭</button>
+      </div>
+    ) : null
+  ),
+}))
+
+jest.mock('@/components/tdesign/icon', () => ({
+  __esModule: true,
+  default: ({ name, size, color }: any) => (
+    <div data-testid="icon" data-name={name} data-size={size} data-color={color}>图标</div>
+  ),
+}))
+
+// Mock AvatarUpload组件
+jest.mock('@/components/ui/avatar-upload', () => ({
+  AvatarUpload: ({
+    currentAvatar,
+    onUploadSuccess,
+    onUploadError,
+    editable
+  }: any) => (
+    <div data-testid="avatar-upload" data-editable={editable}>
+      <img src={currentAvatar} alt="头像" data-testid="avatar-image" />
+      <button
+        data-testid="upload-button"
+        onClick={() => {
+          // 模拟上传成功
+          if (editable) {
+            onUploadSuccess?.({
+              fileUrl: 'https://example.com/new-avatar.jpg',
+              fileId: 456,
+              fileKey: 'avatars/new-avatar.jpg',
+              bucketName: 'd8dai'
+            })
+          }
+        }}
+      >
+        上传头像
+      </button>
+      <button
+        data-testid="upload-error-button"
+        onClick={() => {
+          onUploadError?.(new Error('模拟上传失败'))
+        }}
+      >
+        模拟上传失败
+      </button>
+    </div>
+  ),
+}))
+
+// Mock Taro组件 - 输入框
+jest.mock('@tarojs/components', () => ({
+  View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
+  Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
+  ScrollView: ({ children, ...props }: any) => <div {...props}>{children}</div>,
+  Input: ({ value, onInput, placeholder, maxlength, className }: any) => (
+    <input
+      data-testid="nickname-input"
+      value={value}
+      onChange={(e) => onInput?.({ detail: { value: e.target.value } })}
+      placeholder={placeholder}
+      maxLength={maxlength}
+      className={className}
+    />
+  ),
+}))
+
+// Mock Button组件
+jest.mock('@/components/ui/button', () => ({
+  Button: ({ variant, size, className, onClick, children }: any) => (
+    <button
+      data-testid={`button-${variant}`}
+      data-size={size}
+      className={className}
+      onClick={onClick}
+    >
+      {children}
+    </button>
+  ),
+}))
+
+// 创建测试QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: { retry: false },
+    mutations: { retry: false },
+  },
+})
+
+// Mock CartContext
+jest.mock('@/contexts/CartContext', () => ({
+  CartProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
+  useCart: () => ({
+    cart: {
+      items: [],
+      totalAmount: 0,
+      totalCount: 0,
+    },
+    addToCart: jest.fn(),
+    removeFromCart: jest.fn(),
+    updateQuantity: jest.fn(),
+    clearCart: jest.fn(),
+    isInCart: jest.fn(),
+    getItemQuantity: jest.fn(),
+    isLoading: false,
+  }),
+}))
+
+// 测试包装器
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+  <QueryClientProvider client={createTestQueryClient()}>
+    {children}
+  </QueryClientProvider>
+)
+
+// 测试数据工厂
+const createTestUser = (overrides = {}) => ({
+  id: 1,
+  username: '测试用户',
+  avatarFile: {
+    id: 123,
+    fullUrl: 'https://example.com/avatar.jpg'
+  },
+  ...overrides,
+})
+
+const createTestCreditBalance = (overrides = {}) => ({
+  totalLimit: 1000,
+  usedAmount: 200,
+  availableAmount: 800,
+  isEnabled: true,
+  ...overrides,
+})
+
+describe('个人中心编辑资料功能测试', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+
+    // 设置默认认证状态
+    ;(useAuth as jest.Mock).mockReturnValue({
+      user: createTestUser(),
+      logout: jest.fn(),
+      isLoading: false,
+      updateUser: jest.fn(),
+      refreshUser: jest.fn(),
+    })
+
+    // 设置默认额度查询
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 0 })),
+    })
+  })
+
+  test('应该正确渲染个人中心页面', async () => {
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 验证页面标题
+    await waitFor(() => {
+      expect(screen.getByText('个人中心')).toBeInTheDocument()
+    })
+
+    // 验证用户信息显示
+    expect(screen.getByTestId('user-center-card')).toBeInTheDocument()
+    expect(screen.getByTestId('nickname')).toHaveTextContent('测试用户')
+    expect(screen.getByTestId('avatar')).toHaveTextContent('https://example.com/avatar.jpg')
+  })
+
+  test('应该打开编辑资料弹窗并显示当前头像和昵称', async () => {
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 点击编辑按钮打开弹窗
+    fireEvent.click(screen.getByTestId('edit-button'))
+
+    // 验证弹窗显示
+    expect(screen.getByTestId('popup')).toBeInTheDocument()
+
+    // 验证头像显示正确
+    const avatarImage = screen.getByTestId('avatar-image') as HTMLImageElement
+    expect(avatarImage.src).toBe('https://example.com/avatar.jpg')
+
+    // 验证昵称输入框显示正确
+    const nicknameInput = screen.getByTestId('nickname-input') as HTMLInputElement
+    expect(nicknameInput.value).toBe('测试用户')
+  })
+
+  test('头像上传成功应该调用updateUser和refreshUser', async () => {
+    const mockUpdateUser = jest.fn()
+    const mockRefreshUser = jest.fn()
+
+    // 设置模拟函数
+    ;(useAuth as jest.Mock).mockReturnValue({
+      user: createTestUser(),
+      logout: jest.fn(),
+      isLoading: false,
+      updateUser: mockUpdateUser,
+      refreshUser: mockRefreshUser,
+    })
+    // 设置mock返回值,确保异步流程正常执行
+    mockUpdateUser.mockResolvedValue(createTestUser())
+    mockRefreshUser.mockResolvedValue(createTestUser())
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 打开编辑弹窗
+    fireEvent.click(screen.getByTestId('edit-button'))
+
+    // 点击上传按钮(模拟上传成功)
+    fireEvent.click(screen.getByTestId('upload-button'))
+
+    // 验证updateUser被调用,参数包含新的fileId
+    await waitFor(() => {
+      expect(mockUpdateUser).toHaveBeenCalledWith({ avatarFileId: 456 })
+    })
+
+    // 验证refreshUser被调用
+    await waitFor(() => {
+      expect(mockRefreshUser).toHaveBeenCalled()
+    })
+  })
+
+  test('头像上传失败应该显示错误提示', async () => {
+    // Mock Taro.showToast
+    const mockShowToast = jest.fn()
+    ;(Taro as any).showToast = mockShowToast
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 打开编辑弹窗
+    fireEvent.click(screen.getByTestId('edit-button'))
+
+    // 点击模拟上传失败按钮
+    fireEvent.click(screen.getByTestId('upload-error-button'))
+
+    // 验证错误提示被调用
+    expect(mockShowToast).toHaveBeenCalledWith({
+      title: '模拟上传失败',
+      icon: 'none',
+      duration: 3000,
+    })
+  })
+
+  test('昵称修改应该调用updateUser和refreshUser', async () => {
+    const mockUpdateUser = jest.fn()
+    const mockRefreshUser = jest.fn()
+
+    // 设置模拟函数
+    ;(useAuth as jest.Mock).mockReturnValue({
+      user: createTestUser(),
+      logout: jest.fn(),
+      isLoading: false,
+      updateUser: mockUpdateUser,
+      refreshUser: mockRefreshUser,
+    })
+    // 设置mock返回值,确保异步流程正常执行
+    mockUpdateUser.mockResolvedValue(createTestUser())
+    mockRefreshUser.mockResolvedValue(createTestUser())
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 打开编辑弹窗
+    fireEvent.click(screen.getByTestId('edit-button'))
+
+    // 修改昵称输入框
+    const nicknameInput = screen.getByTestId('nickname-input')
+    fireEvent.change(nicknameInput, { target: { value: '新昵称' } })
+
+    // 点击保存按钮
+    fireEvent.click(screen.getByTestId('button-default'))
+
+    // 验证updateUser被调用,参数包含新昵称
+    await waitFor(() => {
+      expect(mockUpdateUser).toHaveBeenCalledWith({ username: '新昵称' })
+    })
+
+    // 验证refreshUser被调用
+    await waitFor(() => {
+      expect(mockRefreshUser).toHaveBeenCalled()
+    })
+  })
+
+  test('同时修改头像和昵称应该调用updateUser包含两个字段', async () => {
+    const mockUpdateUser = jest.fn()
+    const mockRefreshUser = jest.fn()
+
+    // 设置模拟函数
+    ;(useAuth as jest.Mock).mockReturnValue({
+      user: createTestUser(),
+      logout: jest.fn(),
+      isLoading: false,
+      updateUser: mockUpdateUser,
+      refreshUser: mockRefreshUser,
+    })
+    // 设置mock返回值,确保异步流程正常执行
+    mockUpdateUser.mockResolvedValue(createTestUser())
+    mockRefreshUser.mockResolvedValue(createTestUser())
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 打开编辑弹窗
+    fireEvent.click(screen.getByTestId('edit-button'))
+
+    // 上传新头像
+    fireEvent.click(screen.getByTestId('upload-button'))
+
+    // 修改昵称
+    const nicknameInput = screen.getByTestId('nickname-input')
+    fireEvent.change(nicknameInput, { target: { value: '新昵称' } })
+
+    // 点击保存按钮
+    fireEvent.click(screen.getByTestId('button-default'))
+
+    // 验证updateUser被调用,参数包含两个字段
+    await waitFor(() => {
+      expect(mockUpdateUser).toHaveBeenCalledWith({
+        avatarFileId: 456,
+        username: '新昵称',
+      })
+    })
+
+    // 验证refreshUser被调用
+    await waitFor(() => {
+      expect(mockRefreshUser).toHaveBeenCalled()
+    })
+  })
+})