profile.test.tsx 16 KB

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