edit-profile.test.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. /**
  2. * 个人中心编辑资料功能单元测试
  3. * 测试头像和昵称编辑功能
  4. */
  5. import { render, screen, waitFor, fireEvent } from '@testing-library/react'
  6. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  7. import ProfilePage from '@/pages/profile/index'
  8. import { creditBalanceClient } from '@/api'
  9. import { useAuth } from '@/utils/auth'
  10. import Taro from '@tarojs/taro'
  11. // Mock API客户端
  12. jest.mock('@/api', () => ({
  13. creditBalanceClient: {
  14. me: {
  15. $get: jest.fn(),
  16. },
  17. },
  18. authClient: {
  19. me: {
  20. $get: jest.fn(),
  21. $put: jest.fn(),
  22. },
  23. },
  24. }))
  25. // Mock 认证hook
  26. jest.mock('@/utils/auth', () => ({
  27. useAuth: jest.fn(),
  28. }))
  29. // Mock TDesign组件 - 复用现有测试中的mock
  30. jest.mock('@/components/tdesign/user-center-card', () => ({
  31. __esModule: true,
  32. default: ({ avatar, nickname, isLoggedIn, onUserEdit, className }: any) => (
  33. <div data-testid="user-center-card" className={className}>
  34. <div data-testid="avatar">{avatar}</div>
  35. <div data-testid="nickname">{nickname}</div>
  36. <div data-testid="is-logged-in">{isLoggedIn ? '已登录' : '未登录'}</div>
  37. <button data-testid="edit-button" onClick={onUserEdit}>编辑</button>
  38. </div>
  39. ),
  40. }))
  41. jest.mock('@/components/tdesign/order-group', () => ({
  42. __esModule: true,
  43. default: ({ orderTagInfos, title, desc, onTopClick, onItemClick }: any) => (
  44. <div data-testid="order-group">
  45. <div data-testid="order-title">{title}</div>
  46. <div data-testid="order-desc">{desc}</div>
  47. <button data-testid="top-click" onClick={onTopClick}>查看全部</button>
  48. {orderTagInfos.map((item: any, index: number) => (
  49. <button key={index} data-testid={`order-item-${index}`} onClick={() => onItemClick(item)}>
  50. {item.title}
  51. </button>
  52. ))}
  53. </div>
  54. ),
  55. }))
  56. jest.mock('@/components/tdesign/cell-group', () => ({
  57. __esModule: true,
  58. default: ({ children }: any) => (
  59. <div data-testid="cell-group">{children}</div>
  60. ),
  61. }))
  62. jest.mock('@/components/tdesign/cell', () => ({
  63. __esModule: true,
  64. default: ({ title, bordered, onClick, noteSlot }: any) => (
  65. <div data-testid="cell" data-bordered={bordered}>
  66. <div data-testid="cell-title">{title}</div>
  67. <button data-testid="cell-click" onClick={onClick}>点击</button>
  68. <div data-testid="cell-note">{noteSlot}</div>
  69. </div>
  70. ),
  71. }))
  72. jest.mock('@/components/tdesign/popup', () => ({
  73. __esModule: true,
  74. default: ({ visible, placement, onClose, children }: any) => (
  75. visible ? (
  76. <div data-testid="popup" data-placement={placement}>
  77. {children}
  78. <button data-testid="popup-close" onClick={onClose}>关闭</button>
  79. </div>
  80. ) : null
  81. ),
  82. }))
  83. jest.mock('@/components/tdesign/icon', () => ({
  84. __esModule: true,
  85. default: ({ name, size, color }: any) => (
  86. <div data-testid="icon" data-name={name} data-size={size} data-color={color}>图标</div>
  87. ),
  88. }))
  89. // Mock AvatarUpload组件
  90. jest.mock('@/components/ui/avatar-upload', () => ({
  91. AvatarUpload: ({
  92. currentAvatar,
  93. onUploadSuccess,
  94. onUploadError,
  95. editable
  96. }: any) => (
  97. <div data-testid="avatar-upload" data-editable={editable}>
  98. <img src={currentAvatar} alt="头像" data-testid="avatar-image" />
  99. <button
  100. data-testid="upload-button"
  101. onClick={() => {
  102. // 模拟上传成功
  103. if (editable) {
  104. onUploadSuccess?.({
  105. fileUrl: 'https://example.com/new-avatar.jpg',
  106. fileId: 456,
  107. fileKey: 'avatars/new-avatar.jpg',
  108. bucketName: 'd8dai'
  109. })
  110. }
  111. }}
  112. >
  113. 上传头像
  114. </button>
  115. <button
  116. data-testid="upload-error-button"
  117. onClick={() => {
  118. onUploadError?.(new Error('模拟上传失败'))
  119. }}
  120. >
  121. 模拟上传失败
  122. </button>
  123. </div>
  124. ),
  125. }))
  126. // Mock Taro组件 - 输入框
  127. jest.mock('@tarojs/components', () => ({
  128. View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
  129. Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
  130. ScrollView: ({ children, ...props }: any) => <div {...props}>{children}</div>,
  131. Input: ({ value, onInput, placeholder, maxlength, className }: any) => (
  132. <input
  133. data-testid="nickname-input"
  134. value={value}
  135. onChange={(e) => onInput?.({ detail: { value: e.target.value } })}
  136. placeholder={placeholder}
  137. maxLength={maxlength}
  138. className={className}
  139. />
  140. ),
  141. }))
  142. // Mock Button组件
  143. jest.mock('@/components/ui/button', () => ({
  144. Button: ({ variant, size, className, onClick, children }: any) => (
  145. <button
  146. data-testid={`button-${variant}`}
  147. data-size={size}
  148. className={className}
  149. onClick={onClick}
  150. >
  151. {children}
  152. </button>
  153. ),
  154. }))
  155. // 创建测试QueryClient
  156. const createTestQueryClient = () => new QueryClient({
  157. defaultOptions: {
  158. queries: { retry: false },
  159. mutations: { retry: false },
  160. },
  161. })
  162. // Mock CartContext
  163. jest.mock('@/contexts/CartContext', () => ({
  164. CartProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  165. useCart: () => ({
  166. cart: {
  167. items: [],
  168. totalAmount: 0,
  169. totalCount: 0,
  170. },
  171. addToCart: jest.fn(),
  172. removeFromCart: jest.fn(),
  173. updateQuantity: jest.fn(),
  174. clearCart: jest.fn(),
  175. isInCart: jest.fn(),
  176. getItemQuantity: jest.fn(),
  177. isLoading: false,
  178. }),
  179. }))
  180. // 测试包装器
  181. const TestWrapper = ({ children }: { children: React.ReactNode }) => (
  182. <QueryClientProvider client={createTestQueryClient()}>
  183. {children}
  184. </QueryClientProvider>
  185. )
  186. // 测试数据工厂
  187. const createTestUser = (overrides = {}) => ({
  188. id: 1,
  189. username: '测试用户',
  190. avatarFile: {
  191. id: 123,
  192. fullUrl: 'https://example.com/avatar.jpg'
  193. },
  194. ...overrides,
  195. })
  196. const createTestCreditBalance = (overrides = {}) => ({
  197. totalLimit: 1000,
  198. usedAmount: 200,
  199. availableAmount: 800,
  200. isEnabled: true,
  201. ...overrides,
  202. })
  203. describe('个人中心编辑资料功能测试', () => {
  204. beforeEach(() => {
  205. jest.clearAllMocks()
  206. // 设置默认认证状态
  207. ;(useAuth as jest.Mock).mockReturnValue({
  208. user: createTestUser(),
  209. logout: jest.fn(),
  210. isLoading: false,
  211. updateUser: jest.fn(),
  212. refreshUser: jest.fn(),
  213. })
  214. // 设置默认额度查询
  215. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  216. status: 200,
  217. json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 0 })),
  218. })
  219. })
  220. test('应该正确渲染个人中心页面', async () => {
  221. render(
  222. <TestWrapper>
  223. <ProfilePage />
  224. </TestWrapper>
  225. )
  226. // 验证页面标题
  227. await waitFor(() => {
  228. expect(screen.getByText('个人中心')).toBeInTheDocument()
  229. })
  230. // 验证用户信息显示
  231. expect(screen.getByTestId('user-center-card')).toBeInTheDocument()
  232. expect(screen.getByTestId('nickname')).toHaveTextContent('测试用户')
  233. expect(screen.getByTestId('avatar')).toHaveTextContent('https://example.com/avatar.jpg')
  234. })
  235. test('应该打开编辑资料弹窗并显示当前头像和昵称', async () => {
  236. render(
  237. <TestWrapper>
  238. <ProfilePage />
  239. </TestWrapper>
  240. )
  241. // 点击编辑按钮打开弹窗
  242. fireEvent.click(screen.getByTestId('edit-button'))
  243. // 验证弹窗显示
  244. expect(screen.getByTestId('popup')).toBeInTheDocument()
  245. // 验证头像显示正确
  246. const avatarImage = screen.getByTestId('avatar-image') as HTMLImageElement
  247. expect(avatarImage.src).toBe('https://example.com/avatar.jpg')
  248. // 验证昵称输入框显示正确
  249. const nicknameInput = screen.getByTestId('nickname-input') as HTMLInputElement
  250. expect(nicknameInput.value).toBe('测试用户')
  251. })
  252. test('头像上传成功应该调用updateUser和refreshUser', async () => {
  253. const mockUpdateUser = jest.fn()
  254. const mockRefreshUser = jest.fn()
  255. // 设置模拟函数
  256. ;(useAuth as jest.Mock).mockReturnValue({
  257. user: createTestUser(),
  258. logout: jest.fn(),
  259. isLoading: false,
  260. updateUser: mockUpdateUser,
  261. refreshUser: mockRefreshUser,
  262. })
  263. // 设置mock返回值,确保异步流程正常执行
  264. mockUpdateUser.mockResolvedValue(createTestUser())
  265. mockRefreshUser.mockResolvedValue(createTestUser())
  266. render(
  267. <TestWrapper>
  268. <ProfilePage />
  269. </TestWrapper>
  270. )
  271. // 打开编辑弹窗
  272. fireEvent.click(screen.getByTestId('edit-button'))
  273. // 点击上传按钮(模拟上传成功)
  274. fireEvent.click(screen.getByTestId('upload-button'))
  275. // 验证updateUser被调用,参数包含新的fileId
  276. await waitFor(() => {
  277. expect(mockUpdateUser).toHaveBeenCalledWith({ avatarFileId: 456 })
  278. })
  279. // 验证refreshUser被调用
  280. await waitFor(() => {
  281. expect(mockRefreshUser).toHaveBeenCalled()
  282. })
  283. })
  284. test('头像上传失败应该显示错误提示', async () => {
  285. // Mock Taro.showToast
  286. const mockShowToast = jest.fn()
  287. ;(Taro as any).showToast = mockShowToast
  288. render(
  289. <TestWrapper>
  290. <ProfilePage />
  291. </TestWrapper>
  292. )
  293. // 打开编辑弹窗
  294. fireEvent.click(screen.getByTestId('edit-button'))
  295. // 点击模拟上传失败按钮
  296. fireEvent.click(screen.getByTestId('upload-error-button'))
  297. // 验证错误提示被调用
  298. expect(mockShowToast).toHaveBeenCalledWith({
  299. title: '模拟上传失败',
  300. icon: 'none',
  301. duration: 3000,
  302. })
  303. })
  304. test('昵称修改应该调用updateUser和refreshUser', async () => {
  305. const mockUpdateUser = jest.fn()
  306. const mockRefreshUser = jest.fn()
  307. // 设置模拟函数
  308. ;(useAuth as jest.Mock).mockReturnValue({
  309. user: createTestUser(),
  310. logout: jest.fn(),
  311. isLoading: false,
  312. updateUser: mockUpdateUser,
  313. refreshUser: mockRefreshUser,
  314. })
  315. // 设置mock返回值,确保异步流程正常执行
  316. mockUpdateUser.mockResolvedValue(createTestUser())
  317. mockRefreshUser.mockResolvedValue(createTestUser())
  318. render(
  319. <TestWrapper>
  320. <ProfilePage />
  321. </TestWrapper>
  322. )
  323. // 打开编辑弹窗
  324. fireEvent.click(screen.getByTestId('edit-button'))
  325. // 修改昵称输入框
  326. const nicknameInput = screen.getByTestId('nickname-input')
  327. fireEvent.change(nicknameInput, { target: { value: '新昵称' } })
  328. // 点击保存按钮
  329. fireEvent.click(screen.getByTestId('button-default'))
  330. // 验证updateUser被调用,参数包含新昵称
  331. await waitFor(() => {
  332. expect(mockUpdateUser).toHaveBeenCalledWith({ username: '新昵称' })
  333. })
  334. // 验证refreshUser被调用
  335. await waitFor(() => {
  336. expect(mockRefreshUser).toHaveBeenCalled()
  337. })
  338. })
  339. test('同时修改头像和昵称应该调用updateUser包含两个字段', async () => {
  340. const mockUpdateUser = jest.fn()
  341. const mockRefreshUser = jest.fn()
  342. // 设置模拟函数
  343. ;(useAuth as jest.Mock).mockReturnValue({
  344. user: createTestUser(),
  345. logout: jest.fn(),
  346. isLoading: false,
  347. updateUser: mockUpdateUser,
  348. refreshUser: mockRefreshUser,
  349. })
  350. // 设置mock返回值,确保异步流程正常执行
  351. mockUpdateUser.mockResolvedValue(createTestUser())
  352. mockRefreshUser.mockResolvedValue(createTestUser())
  353. render(
  354. <TestWrapper>
  355. <ProfilePage />
  356. </TestWrapper>
  357. )
  358. // 打开编辑弹窗
  359. fireEvent.click(screen.getByTestId('edit-button'))
  360. // 上传新头像
  361. fireEvent.click(screen.getByTestId('upload-button'))
  362. // 修改昵称
  363. const nicknameInput = screen.getByTestId('nickname-input')
  364. fireEvent.change(nicknameInput, { target: { value: '新昵称' } })
  365. // 点击保存按钮
  366. fireEvent.click(screen.getByTestId('button-default'))
  367. // 验证updateUser被调用,参数包含两个字段
  368. await waitFor(() => {
  369. expect(mockUpdateUser).toHaveBeenCalledWith({
  370. avatarFileId: 456,
  371. username: '新昵称',
  372. })
  373. })
  374. // 验证refreshUser被调用
  375. await waitFor(() => {
  376. expect(mockRefreshUser).toHaveBeenCalled()
  377. })
  378. })
  379. })