basic.test.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import React from 'react'
  2. import { render, screen, fireEvent, waitFor } from '@testing-library/react'
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  4. import SearchResultPage from '@/pages/search-result/index'
  5. // 导入Taro mock函数
  6. import {
  7. mockNavigateTo,
  8. mockGetCurrentInstance,
  9. mockStopPullDownRefresh
  10. } from '~/__mocks__/taroMock'
  11. // Mock components
  12. jest.mock('@/components/ui/navbar', () => ({
  13. Navbar: ({ title, onClickLeft }: { title: string; onClickLeft: () => void }) => (
  14. <div data-testid="navbar">
  15. <span>{title}</span>
  16. <button onClick={onClickLeft}>返回</button>
  17. </div>
  18. ),
  19. }))
  20. jest.mock('@/components/goods-list', () => ({
  21. __esModule: true,
  22. default: ({
  23. goodsList,
  24. onClick,
  25. onAddCart
  26. }: {
  27. goodsList: any[],
  28. onClick: (goods: any) => void,
  29. onAddCart: (goods: any) => void
  30. }) => (
  31. <div data-testid="goods-list">
  32. {goodsList.map((goods, index) => (
  33. <div
  34. key={goods.id}
  35. data-testid={`goods-item-${index}`}
  36. onClick={() => onClick(goods)}
  37. >
  38. <span data-testid="goods-name">{goods.name}</span>
  39. <span data-testid="goods-price">{goods.price}</span>
  40. <button
  41. data-testid="add-cart-btn"
  42. onClick={() => onAddCart(goods)}
  43. >
  44. 加入购物车
  45. </button>
  46. </div>
  47. ))}
  48. </div>
  49. ),
  50. }))
  51. // Mock API client
  52. jest.mock('@/api', () => ({
  53. goodsClient: {
  54. $get: jest.fn()
  55. }
  56. }))
  57. // Mock cart hook
  58. const mockAddToCart = jest.fn()
  59. jest.mock('@/utils/cart', () => ({
  60. useCart: () => ({
  61. addToCart: mockAddToCart
  62. })
  63. }))
  64. describe('SearchResultPage', () => {
  65. let queryClient: QueryClient
  66. beforeEach(() => {
  67. queryClient = new QueryClient({
  68. defaultOptions: {
  69. queries: { retry: false },
  70. mutations: { retry: false },
  71. },
  72. })
  73. // Reset all mocks
  74. jest.clearAllMocks()
  75. mockAddToCart.mockClear()
  76. // Mock Taro.getCurrentInstance
  77. mockGetCurrentInstance.mockReturnValue({
  78. router: {
  79. params: {
  80. keyword: '手机'
  81. }
  82. }
  83. })
  84. // Mock API response
  85. const { goodsClient } = require('@/api')
  86. goodsClient.$get.mockResolvedValue({
  87. status: 200,
  88. json: () => Promise.resolve({
  89. data: [
  90. {
  91. id: 1,
  92. name: 'iPhone 15',
  93. price: 599900,
  94. originPrice: 699900,
  95. stock: 10,
  96. salesNum: 100,
  97. imageFile: {
  98. fullUrl: 'https://example.com/iphone15.jpg'
  99. }
  100. },
  101. {
  102. id: 2,
  103. name: 'MacBook Pro',
  104. price: 1299900,
  105. originPrice: 1499900,
  106. stock: 5,
  107. salesNum: 50,
  108. imageFile: {
  109. fullUrl: 'https://example.com/macbook.jpg'
  110. }
  111. }
  112. ],
  113. pagination: {
  114. current: 1,
  115. pageSize: 10,
  116. total: 2,
  117. totalPages: 1
  118. }
  119. })
  120. })
  121. })
  122. const renderWithProviders = (component: React.ReactElement) => {
  123. return render(
  124. <QueryClientProvider client={queryClient}>
  125. {component}
  126. </QueryClientProvider>
  127. )
  128. }
  129. it('渲染页面标题和布局', async () => {
  130. renderWithProviders(<SearchResultPage />)
  131. await waitFor(() => {
  132. expect(screen.getByTestId('navbar')).toBeInTheDocument()
  133. expect(screen.getByTestId('navbar')).toHaveTextContent('搜索结果')
  134. })
  135. })
  136. it('显示搜索栏和关键词', async () => {
  137. renderWithProviders(<SearchResultPage />)
  138. await waitFor(() => {
  139. // 验证搜索栏存在
  140. const searchInput = document.querySelector('.search-input') as HTMLInputElement
  141. expect(searchInput).toBeInTheDocument()
  142. expect(searchInput.placeholder).toBe('搜索商品...')
  143. expect(searchInput.value).toBe('手机')
  144. // 验证搜索结果标题
  145. expect(screen.getByText('搜索结果:"手机"')).toBeInTheDocument()
  146. expect(screen.getByText('共找到 2 件商品')).toBeInTheDocument()
  147. })
  148. })
  149. it('显示搜索结果列表', async () => {
  150. renderWithProviders(<SearchResultPage />)
  151. await waitFor(() => {
  152. expect(screen.getByTestId('goods-list')).toBeInTheDocument()
  153. const goodsItems = screen.getAllByTestId(/goods-item-\d+/)
  154. expect(goodsItems).toHaveLength(2)
  155. expect(screen.getByText('iPhone 15')).toBeInTheDocument()
  156. expect(screen.getByText('MacBook Pro')).toBeInTheDocument()
  157. })
  158. })
  159. it('处理搜索提交', async () => {
  160. renderWithProviders(<SearchResultPage />)
  161. await waitFor(() => {
  162. const searchInput = document.querySelector('.search-input') as HTMLInputElement
  163. expect(searchInput).toBeInTheDocument()
  164. })
  165. // 修改搜索关键词
  166. const searchInput = document.querySelector('.search-input') as HTMLInputElement
  167. fireEvent.change(searchInput, { target: { value: 'iPad' } })
  168. // 提交搜索
  169. fireEvent.keyPress(searchInput, { key: 'Enter', code: 'Enter' })
  170. // 验证搜索输入框的值被更新
  171. await waitFor(() => {
  172. expect(searchInput.value).toBe('iPad')
  173. })
  174. // 验证清除按钮出现(表示有输入内容)
  175. await waitFor(() => {
  176. const clearIcon = document.querySelector('.clear-icon')
  177. expect(clearIcon).toBeInTheDocument()
  178. })
  179. })
  180. it('显示空状态', async () => {
  181. // Mock empty response
  182. const { goodsClient } = require('@/api')
  183. goodsClient.$get.mockResolvedValue({
  184. status: 200,
  185. json: () => Promise.resolve({
  186. data: [],
  187. pagination: {
  188. current: 1,
  189. pageSize: 10,
  190. total: 0,
  191. totalPages: 0
  192. }
  193. })
  194. })
  195. renderWithProviders(<SearchResultPage />)
  196. await waitFor(() => {
  197. expect(screen.getByText('暂无相关商品')).toBeInTheDocument()
  198. expect(screen.getByText('换个关键词试试吧')).toBeInTheDocument()
  199. })
  200. })
  201. it('处理商品点击', async () => {
  202. renderWithProviders(<SearchResultPage />)
  203. await waitFor(() => {
  204. const firstGoodsItem = screen.getByTestId('goods-item-0')
  205. fireEvent.click(firstGoodsItem)
  206. })
  207. // 验证跳转到商品详情页面
  208. expect(mockNavigateTo).toHaveBeenCalledWith({
  209. url: '/pages/goods-detail/index?id=1'
  210. })
  211. })
  212. it('处理添加到购物车', async () => {
  213. renderWithProviders(<SearchResultPage />)
  214. await waitFor(() => {
  215. const addCartButtons = screen.getAllByTestId('add-cart-btn')
  216. expect(addCartButtons.length).toBeGreaterThan(0)
  217. fireEvent.click(addCartButtons[0])
  218. })
  219. // 验证购物车功能被调用
  220. expect(mockAddToCart).toHaveBeenCalledWith({
  221. id: 1,
  222. name: 'iPhone 15',
  223. price: 599900,
  224. image: 'https://example.com/iphone15.jpg',
  225. stock: 10,
  226. quantity: 1
  227. })
  228. })
  229. it('处理下拉刷新', async () => {
  230. renderWithProviders(<SearchResultPage />)
  231. await waitFor(() => {
  232. // 模拟下拉刷新 - 直接调用onRefresherRefresh
  233. const scrollView = document.querySelector('.search-result-content')
  234. if (scrollView) {
  235. // 触发下拉刷新事件
  236. const event = new Event('refresherrefresh')
  237. scrollView.dispatchEvent(event)
  238. }
  239. })
  240. // 验证API被重新调用
  241. await waitFor(() => {
  242. const { goodsClient } = require('@/api')
  243. expect(goodsClient.$get).toHaveBeenCalled()
  244. })
  245. })
  246. it('处理清除搜索输入', async () => {
  247. renderWithProviders(<SearchResultPage />)
  248. await waitFor(() => {
  249. const searchInput = document.querySelector('.search-input') as HTMLInputElement
  250. // 输入内容
  251. fireEvent.change(searchInput, { target: { value: '测试商品' } })
  252. // 验证清除按钮出现
  253. const clearIcon = document.querySelector('.clear-icon')
  254. expect(clearIcon).toBeInTheDocument()
  255. // 点击清除按钮
  256. fireEvent.click(clearIcon!)
  257. // 验证搜索输入被清空
  258. expect(searchInput.value).toBe('')
  259. })
  260. })
  261. it('验证样式类名应用', async () => {
  262. renderWithProviders(<SearchResultPage />)
  263. await waitFor(() => {
  264. const container = document.querySelector('.search-result-page')
  265. expect(container).toBeInTheDocument()
  266. const content = document.querySelector('.search-result-content')
  267. expect(content).toBeInTheDocument()
  268. const searchBar = document.querySelector('.search-bar-container')
  269. expect(searchBar).toBeInTheDocument()
  270. const resultContainer = document.querySelector('.result-container')
  271. expect(resultContainer).toBeInTheDocument()
  272. const goodsListContainer = document.querySelector('.goods-list-container')
  273. expect(goodsListContainer).toBeInTheDocument()
  274. })
  275. })
  276. it('显示加载状态', async () => {
  277. // Mock loading state
  278. const { goodsClient } = require('@/api')
  279. goodsClient.$get.mockImplementation(() => new Promise(() => {})) // Never resolves
  280. renderWithProviders(<SearchResultPage />)
  281. await waitFor(() => {
  282. expect(screen.getByText('搜索中...')).toBeInTheDocument()
  283. })
  284. })
  285. })