profile.test.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. /**
  2. * 个人中心页面组件测试
  3. */
  4. import React from 'react'
  5. import { render, screen, fireEvent, waitFor } from '@testing-library/react'
  6. import '@testing-library/jest-dom'
  7. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  8. import ProfilePage from '../../src/pages/profile/index'
  9. // 导入 Taro mock 函数
  10. import taroMock from '../../tests/__mocks__/taroMock'
  11. // Mock TabBarLayout 组件
  12. jest.mock('@/layouts/tab-bar-layout', () => ({
  13. TabBarLayout: jest.fn(({ children, activeKey, className }) => (
  14. <div data-testid="tab-bar-layout" data-active-key={activeKey} className={className}>
  15. {children}
  16. </div>
  17. ))
  18. }))
  19. // Mock Navbar 组件
  20. jest.mock('@/components/ui/navbar', () => ({
  21. Navbar: jest.fn(({ title, rightIcon, onClickRight, leftIcon, backgroundColor, textColor, border }) => (
  22. <div
  23. data-testid="navbar"
  24. data-title={title}
  25. data-right-icon={rightIcon}
  26. data-left-icon={leftIcon}
  27. data-background-color={backgroundColor}
  28. data-text-color={textColor}
  29. data-border={border}
  30. >
  31. <button data-testid="navbar-right-button" onClick={onClickRight}>
  32. {rightIcon}
  33. </button>
  34. <h1>{title}</h1>
  35. </div>
  36. ))
  37. }))
  38. // Mock AvatarUpload 组件
  39. jest.mock('@/components/ui/avatar-upload', () => ({
  40. AvatarUpload: jest.fn(({ currentAvatar, onUploadSuccess, onUploadError, size, editable, className }) => (
  41. <div
  42. data-testid="avatar-upload"
  43. data-current-avatar={currentAvatar}
  44. data-size={size}
  45. data-editable={editable}
  46. className={className}
  47. >
  48. <button
  49. data-testid="avatar-upload-button"
  50. onClick={() => onUploadSuccess({ fileId: 'test-file-id', fullUrl: 'https://example.com/avatar.jpg' })}
  51. >
  52. 上传头像
  53. </button>
  54. <button
  55. data-testid="avatar-upload-error-button"
  56. onClick={() => onUploadError(new Error('Upload failed'))}
  57. >
  58. 上传失败
  59. </button>
  60. </div>
  61. ))
  62. }))
  63. // Mock Button 组件
  64. jest.mock('@/components/ui/button', () => ({
  65. Button: jest.fn(({ children, variant, size, onClick, className }) => (
  66. <button
  67. data-testid="button"
  68. data-variant={variant}
  69. data-size={size}
  70. className={className}
  71. onClick={onClick}
  72. >
  73. {children}
  74. </button>
  75. ))
  76. }))
  77. // Mock useAuth hook
  78. const mockUser = {
  79. id: 1,
  80. username: '测试用户',
  81. avatarFile: {
  82. fullUrl: 'https://example.com/avatar.jpg'
  83. }
  84. }
  85. const mockLogout = jest.fn()
  86. const mockUpdateUser = jest.fn()
  87. jest.mock('@/utils/auth', () => ({
  88. useAuth: jest.fn(() => ({
  89. user: mockUser,
  90. logout: mockLogout,
  91. isLoading: false,
  92. updateUser: mockUpdateUser
  93. }))
  94. }))
  95. // Mock React Query hooks
  96. const mockUseQuery = jest.fn()
  97. const mockUseMutation = jest.fn()
  98. jest.mock('@tanstack/react-query', () => {
  99. const actual = jest.requireActual('@tanstack/react-query')
  100. return {
  101. ...actual,
  102. useQuery: (options: any) => mockUseQuery(options),
  103. useMutation: (options: any) => mockUseMutation(options)
  104. }
  105. })
  106. // 创建测试用的 QueryClient
  107. const createTestQueryClient = () => new QueryClient({
  108. defaultOptions: {
  109. queries: {
  110. retry: false,
  111. },
  112. },
  113. })
  114. // 包装组件
  115. const Wrapper = ({ children }: { children: React.ReactNode }) => {
  116. const queryClient = createTestQueryClient()
  117. return (
  118. <QueryClientProvider client={queryClient}>
  119. {children}
  120. </QueryClientProvider>
  121. )
  122. }
  123. describe('个人中心页面测试', () => {
  124. beforeEach(() => {
  125. jest.clearAllMocks()
  126. // 设置环境变量
  127. process.env.TARO_APP_WX_CORP_ID = 'wwc6d7911e2d23b7fb'
  128. process.env.TARO_APP_WX_KEFU_URL = 'https://work.weixin.qq.com/kfid/kfc5f4d729bc3c893d7'
  129. // 初始化 React Query mock
  130. mockUseQuery.mockImplementation(() => ({
  131. data: null,
  132. isLoading: false
  133. }))
  134. mockUseMutation.mockImplementation((options) => ({
  135. mutateAsync: options.mutationFn,
  136. isPending: false
  137. }))
  138. // 重置所有 mock 调用记录
  139. taroMock.showToast.mockClear()
  140. taroMock.openCustomerServiceChat.mockClear()
  141. taroMock.navigateTo.mockClear()
  142. taroMock.showLoading.mockClear()
  143. taroMock.hideLoading.mockClear()
  144. taroMock.showModal.mockClear()
  145. taroMock.reLaunch.mockClear()
  146. })
  147. test('应该正确渲染个人中心页面', () => {
  148. render(
  149. <Wrapper>
  150. <ProfilePage />
  151. </Wrapper>
  152. )
  153. // 检查页面标题
  154. expect(screen.getByText('个人中心')).toBeInTheDocument()
  155. // 检查用户信息
  156. expect(screen.getByText('普通用户')).toBeInTheDocument()
  157. expect(screen.getByText('ID: 1')).toBeInTheDocument()
  158. // 检查功能菜单
  159. expect(screen.getByText('我的服务')).toBeInTheDocument()
  160. expect(screen.getByText('编辑资料')).toBeInTheDocument()
  161. expect(screen.getByText('乘车人管理')).toBeInTheDocument()
  162. expect(screen.getByText('设置')).toBeInTheDocument()
  163. expect(screen.getByText('隐私政策')).toBeInTheDocument()
  164. expect(screen.getByText('帮助与反馈')).toBeInTheDocument()
  165. // 检查客服与帮助区域
  166. expect(screen.getByText('客服与帮助')).toBeInTheDocument()
  167. expect(screen.getByText('联系客服')).toBeInTheDocument()
  168. expect(screen.getByText('7x24小时在线客服')).toBeInTheDocument()
  169. expect(screen.getByText('常见问题')).toBeInTheDocument()
  170. expect(screen.getByText('意见反馈')).toBeInTheDocument()
  171. // 检查版本信息
  172. expect(screen.getByText('去看出行 v1.0.0')).toBeInTheDocument()
  173. })
  174. test('应该处理联系客服功能 - 成功场景', async () => {
  175. taroMock.openCustomerServiceChat.mockImplementation((options) => {
  176. options.success()
  177. })
  178. render(
  179. <Wrapper>
  180. <ProfilePage />
  181. </Wrapper>
  182. )
  183. // 点击联系客服按钮
  184. const customerServiceButton = screen.getByTestId('customer-service-button')
  185. fireEvent.click(customerServiceButton)
  186. // 检查微信客服API被正确调用
  187. await waitFor(() => {
  188. expect(taroMock.openCustomerServiceChat).toHaveBeenCalledWith({
  189. extInfo: {
  190. url: 'https://work.weixin.qq.com/kfid/kfc5f4d729bc3c893d7'
  191. },
  192. corpId: 'wwc6d7911e2d23b7fb',
  193. success: expect.any(Function),
  194. fail: expect.any(Function)
  195. })
  196. })
  197. })
  198. test('应该处理联系客服功能 - 失败场景', async () => {
  199. taroMock.openCustomerServiceChat.mockImplementation((options) => {
  200. options.fail({ errMsg: '客服功能不可用' })
  201. })
  202. render(
  203. <Wrapper>
  204. <ProfilePage />
  205. </Wrapper>
  206. )
  207. // 点击联系客服按钮
  208. const customerServiceButton = screen.getByTestId('customer-service-button')
  209. fireEvent.click(customerServiceButton)
  210. // 检查错误提示显示
  211. await waitFor(() => {
  212. expect(taroMock.showToast).toHaveBeenCalledWith({
  213. title: '客服功能暂不可用,请稍后重试',
  214. icon: 'none'
  215. })
  216. })
  217. })
  218. test('应该处理联系客服功能 - 异常场景', async () => {
  219. taroMock.openCustomerServiceChat.mockImplementation(() => {
  220. throw new Error('API调用异常')
  221. })
  222. render(
  223. <Wrapper>
  224. <ProfilePage />
  225. </Wrapper>
  226. )
  227. // 点击联系客服按钮
  228. const customerServiceButton = screen.getByTestId('customer-service-button')
  229. fireEvent.click(customerServiceButton)
  230. // 检查异常处理
  231. await waitFor(() => {
  232. expect(taroMock.showToast).toHaveBeenCalledWith({
  233. title: '客服功能异常,请稍后重试',
  234. icon: 'none'
  235. })
  236. })
  237. })
  238. test('应该处理其他功能按钮点击', () => {
  239. render(
  240. <Wrapper>
  241. <ProfilePage />
  242. </Wrapper>
  243. )
  244. // 点击编辑资料按钮
  245. const editProfileButton = screen.getByTestId('edit-profile-button')
  246. fireEvent.click(editProfileButton)
  247. expect(taroMock.showToast).toHaveBeenCalledWith({
  248. title: '功能开发中...',
  249. icon: 'none'
  250. })
  251. // 点击乘车人管理按钮
  252. const passengersButton = screen.getByTestId('passengers-button')
  253. fireEvent.click(passengersButton)
  254. expect(taroMock.navigateTo).toHaveBeenCalledWith({
  255. url: '/pages/passengers/passengers'
  256. })
  257. // 点击设置按钮
  258. const settingsButton = screen.getByTestId('settings-button')
  259. fireEvent.click(settingsButton)
  260. expect(taroMock.showToast).toHaveBeenCalledWith({
  261. title: '功能开发中...',
  262. icon: 'none'
  263. })
  264. // 点击常见问题按钮
  265. const faqButton = screen.getByTestId('faq-button')
  266. fireEvent.click(faqButton)
  267. expect(taroMock.showToast).toHaveBeenCalledWith({
  268. title: '常见问题功能开发中...',
  269. icon: 'none'
  270. })
  271. // 点击意见反馈按钮
  272. const feedbackButton = screen.getByTestId('feedback-button')
  273. fireEvent.click(feedbackButton)
  274. expect(taroMock.showToast).toHaveBeenCalledWith({
  275. title: '意见反馈功能开发中...',
  276. icon: 'none'
  277. })
  278. })
  279. test('应该处理头像上传功能', async () => {
  280. render(
  281. <Wrapper>
  282. <ProfilePage />
  283. </Wrapper>
  284. )
  285. // 点击头像上传成功按钮
  286. const uploadButton = screen.getByTestId('avatar-upload-button')
  287. fireEvent.click(uploadButton)
  288. // 检查上传成功处理
  289. await waitFor(() => {
  290. expect(taroMock.showLoading).toHaveBeenCalledWith({ title: '更新头像...' })
  291. expect(taroMock.hideLoading).toHaveBeenCalled()
  292. expect(taroMock.showToast).toHaveBeenCalledWith({
  293. title: '头像更新成功',
  294. icon: 'success'
  295. })
  296. expect(mockUpdateUser).toHaveBeenCalledWith({
  297. ...mockUser,
  298. avatarFileId: 'test-file-id'
  299. })
  300. })
  301. })
  302. test('应该处理头像上传失败', async () => {
  303. render(
  304. <Wrapper>
  305. <ProfilePage />
  306. </Wrapper>
  307. )
  308. // 点击头像上传失败按钮
  309. const uploadErrorButton = screen.getByTestId('avatar-upload-error-button')
  310. fireEvent.click(uploadErrorButton)
  311. // 检查上传失败处理
  312. await waitFor(() => {
  313. expect(taroMock.showToast).toHaveBeenCalledWith({
  314. title: '上传失败,请重试',
  315. icon: 'none'
  316. })
  317. })
  318. })
  319. // 退出登录功能暂时被注释掉,跳过相关测试
  320. test.skip('应该处理退出登录', async () => {
  321. taroMock.showModal.mockImplementation((options) => {
  322. options.success({ confirm: true })
  323. })
  324. render(
  325. <Wrapper>
  326. <ProfilePage />
  327. </Wrapper>
  328. )
  329. // 点击退出登录按钮
  330. const logoutButton = screen.getByText('退出登录')
  331. fireEvent.click(logoutButton)
  332. // 检查退出登录流程
  333. await waitFor(() => {
  334. expect(taroMock.showModal).toHaveBeenCalledWith({
  335. title: '退出登录',
  336. content: '确定要退出登录吗?',
  337. success: expect.any(Function)
  338. })
  339. expect(taroMock.showLoading).toHaveBeenCalledWith({ title: '退出中...' })
  340. expect(mockLogout).toHaveBeenCalled()
  341. expect(taroMock.hideLoading).toHaveBeenCalled()
  342. expect(taroMock.showToast).toHaveBeenCalledWith({
  343. title: '已退出登录',
  344. icon: 'success',
  345. duration: 1500
  346. })
  347. })
  348. })
  349. test.skip('应该处理退出登录取消', async () => {
  350. taroMock.showModal.mockImplementation((options) => {
  351. options.success({ confirm: false })
  352. })
  353. render(
  354. <Wrapper>
  355. <ProfilePage />
  356. </Wrapper>
  357. )
  358. // 点击退出登录按钮
  359. const logoutButton = screen.getByText('退出登录')
  360. fireEvent.click(logoutButton)
  361. // 检查取消退出登录
  362. await waitFor(() => {
  363. expect(taroMock.showModal).toHaveBeenCalled()
  364. expect(mockLogout).not.toHaveBeenCalled()
  365. })
  366. })
  367. test('应该正确使用TabBarLayout', () => {
  368. render(
  369. <Wrapper>
  370. <ProfilePage />
  371. </Wrapper>
  372. )
  373. // 检查TabBarLayout是否正确使用
  374. const tabBarLayout = screen.getByTestId('tab-bar-layout')
  375. expect(tabBarLayout).toHaveAttribute('data-active-key', 'profile')
  376. })
  377. test('应该正确使用Navbar', () => {
  378. render(
  379. <Wrapper>
  380. <ProfilePage />
  381. </Wrapper>
  382. )
  383. // 检查Navbar是否正确使用
  384. const navbar = screen.getByTestId('navbar')
  385. expect(navbar).toHaveAttribute('data-title', '个人中心')
  386. expect(navbar).toHaveAttribute('data-right-icon', 'i-heroicons-cog-6-tooth-20-solid')
  387. expect(navbar).toHaveAttribute('data-background-color', 'bg-primary')
  388. expect(navbar).toHaveAttribute('data-text-color', 'text-white')
  389. expect(navbar).toHaveAttribute('data-border', 'false')
  390. })
  391. test('应该显示默认头像当用户无头像时', () => {
  392. // 模拟用户无头像的情况
  393. const mockUseAuth = jest.requireMock('@/utils/auth').useAuth
  394. mockUseAuth.mockImplementation(() => ({
  395. user: {
  396. ...mockUser,
  397. avatarFile: null
  398. },
  399. logout: mockLogout,
  400. isLoading: false,
  401. updateUser: mockUpdateUser
  402. }))
  403. render(
  404. <Wrapper>
  405. <ProfilePage />
  406. </Wrapper>
  407. )
  408. // 检查默认头像路径
  409. const avatarUpload = screen.getByTestId('avatar-upload')
  410. expect(avatarUpload).toHaveAttribute('data-current-avatar', '/images/default_avatar.jpg')
  411. })
  412. test('应该显示默认用户名', () => {
  413. // 模拟用户无用户名的情况
  414. const mockUseAuth = jest.requireMock('@/utils/auth').useAuth
  415. mockUseAuth.mockImplementation(() => ({
  416. user: {
  417. ...mockUser,
  418. username: ''
  419. },
  420. logout: mockLogout,
  421. isLoading: false,
  422. updateUser: mockUpdateUser
  423. }))
  424. render(
  425. <Wrapper>
  426. <ProfilePage />
  427. </Wrapper>
  428. )
  429. // 检查用户名显示为"普通用户"
  430. expect(screen.getByText('普通用户')).toBeInTheDocument()
  431. })
  432. test('头像上传功能应该在默认头像状态下正常工作', async () => {
  433. // 模拟用户无头像的情况
  434. const mockUseAuth = jest.requireMock('@/utils/auth').useAuth
  435. mockUseAuth.mockImplementation(() => ({
  436. user: {
  437. ...mockUser,
  438. avatarFile: null
  439. },
  440. logout: mockLogout,
  441. isLoading: false,
  442. updateUser: mockUpdateUser
  443. }))
  444. render(
  445. <Wrapper>
  446. <ProfilePage />
  447. </Wrapper>
  448. )
  449. // 点击头像上传成功按钮
  450. const uploadButton = screen.getByTestId('avatar-upload-button')
  451. fireEvent.click(uploadButton)
  452. // 检查上传成功处理
  453. await waitFor(() => {
  454. expect(taroMock.showLoading).toHaveBeenCalledWith({ title: '更新头像...' })
  455. expect(taroMock.hideLoading).toHaveBeenCalled()
  456. expect(taroMock.showToast).toHaveBeenCalledWith({
  457. title: '头像更新成功',
  458. icon: 'success'
  459. })
  460. expect(mockUpdateUser).toHaveBeenCalledWith({
  461. ...mockUser,
  462. avatarFile: null,
  463. avatarFileId: 'test-file-id'
  464. })
  465. })
  466. })
  467. })