CartContext.test.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import React from 'react'
  2. import { render } from '@testing-library/react'
  3. import { CartProvider, useCart, CartItem } from '@/contexts/CartContext'
  4. import { mockShowToast, mockGetStorageSync, mockSetStorageSync } from '~/__mocks__/taroMock'
  5. // Mock Taro API
  6. jest.mock('@tarojs/taro', () => jest.requireActual('~/__mocks__/taroMock'))
  7. // 测试组件用于访问购物车hook
  8. const TestComponent = ({ action, item }: { action: string; item?: CartItem }) => {
  9. const cart = useCart()
  10. React.useEffect(() => {
  11. console.log('TestComponent useEffect called, action:', action, 'item:', item, 'isLoading:', cart.isLoading)
  12. // 等待购物车加载完成后再添加商品
  13. if (!cart.isLoading && action === 'add' && item) {
  14. console.log('Calling addToCart with item:', item)
  15. cart.addToCart(item)
  16. }
  17. }, [action, item, cart.isLoading])
  18. console.log('TestComponent rendering, cart items:', cart.cart.items, 'isLoading:', cart.isLoading)
  19. return (
  20. <div>
  21. <div data-testid="items-count">{cart.cart.items.length}</div>
  22. <div data-testid="total-count">{cart.cart.totalCount}</div>
  23. <div data-testid="total-amount">{cart.cart.totalAmount}</div>
  24. {cart.cart.items.map((item, index) => (
  25. <div key={index} data-testid={`item-${index}`}>
  26. <span data-testid={`item-${index}-id`}>{item.id}</span>
  27. <span data-testid={`item-${index}-name`}>{item.name}</span>
  28. {/* spec字段已移除 */}
  29. <span data-testid={`item-${index}-quantity`}>{item.quantity}</span>
  30. <span data-testid={`item-${index}-stock`} style={{ display: 'none' }}>{item.stock}</span>
  31. </div>
  32. ))}
  33. </div>
  34. )
  35. }
  36. describe('CartContext - 规格支持', () => {
  37. beforeEach(() => {
  38. mockGetStorageSync.mockReturnValue(null)
  39. mockSetStorageSync.mockClear()
  40. mockShowToast.mockClear()
  41. })
  42. it('应该支持添加父商品到购物车', () => {
  43. const parentGoods: CartItem = {
  44. id: 1001,
  45. parentGoodsId: 0, // 父商品,无父商品
  46. name: '测试父商品',
  47. price: 99.9,
  48. image: 'parent.jpg',
  49. stock: 10,
  50. quantity: 2,
  51. }
  52. const { getByTestId } = render(
  53. <CartProvider>
  54. <TestComponent action="add" item={parentGoods} />
  55. </CartProvider>
  56. )
  57. expect(getByTestId('items-count').textContent).toBe('1')
  58. expect(getByTestId('item-0-id').textContent).toBe('1001')
  59. expect(getByTestId('item-0-name').textContent).toBe('测试父商品')
  60. expect(mockSetStorageSync).toHaveBeenCalled()
  61. })
  62. it('应该支持添加子商品(带规格)到购物车', () => {
  63. const childGoods: CartItem = {
  64. id: 2001, // 子商品ID
  65. parentGoodsId: 2000, // 父商品ID
  66. name: '红色/M', // 规格名称
  67. price: 109.9,
  68. image: 'child.jpg',
  69. stock: 5,
  70. quantity: 1,
  71. }
  72. const { getByTestId } = render(
  73. <CartProvider>
  74. <TestComponent action="add" item={childGoods} />
  75. </CartProvider>
  76. )
  77. expect(getByTestId('items-count').textContent).toBe('1')
  78. expect(getByTestId('item-0-id').textContent).toBe('2001')
  79. expect(getByTestId('item-0-name').textContent).toBe('红色/M')
  80. expect(mockSetStorageSync).toHaveBeenCalled()
  81. })
  82. it('应该支持添加同一子商品多次(数量累加)', () => {
  83. const childGoods1: CartItem = {
  84. id: 3001,
  85. parentGoodsId: 3000, // 父商品ID
  86. name: '蓝色/L', // 规格名称
  87. price: 89.9,
  88. image: 'goods.jpg',
  89. stock: 10,
  90. quantity: 1,
  91. }
  92. const childGoods2: CartItem = {
  93. id: 3001, // 同一子商品ID
  94. parentGoodsId: 3000, // 父商品ID
  95. name: '蓝色/L', // 规格名称
  96. price: 89.9,
  97. image: 'goods.jpg',
  98. stock: 10,
  99. quantity: 3,
  100. }
  101. const { getByTestId, rerender } = render(
  102. <CartProvider>
  103. <TestComponent action="add" item={childGoods1} />
  104. </CartProvider>
  105. )
  106. expect(getByTestId('items-count').textContent).toBe('1')
  107. console.log('Item 0 id:', getByTestId('item-0-id').textContent)
  108. console.log('Item 0 name:', getByTestId('item-0-name').textContent)
  109. const quantityElement = getByTestId('item-0-quantity')
  110. console.log('Quantity element text:', quantityElement.textContent)
  111. // 修复:检查数量是否正确,应该是1而不是库存值10
  112. expect(parseInt(quantityElement.textContent || '0')).toBe(1)
  113. // 重新渲染添加更多数量
  114. rerender(
  115. <CartProvider>
  116. <TestComponent action="add" item={childGoods2} />
  117. </CartProvider>
  118. )
  119. expect(getByTestId('item-0-quantity').textContent).toBe('4') // 1 + 3
  120. })
  121. it('应该限制数量不超过库存', () => {
  122. const childGoods: CartItem = {
  123. id: 4001,
  124. parentGoodsId: 4000, // 父商品ID
  125. name: '测试商品 - 黑色/XL',
  126. price: 129.9,
  127. image: 'goods.jpg',
  128. stock: 2, // 库存只有2
  129. quantity: 3, // 尝试购买3个
  130. spec: '黑色/XL',
  131. }
  132. const { getByTestId } = render(
  133. <CartProvider>
  134. <TestComponent action="add" item={childGoods} />
  135. </CartProvider>
  136. )
  137. // 应该显示库存不足提示
  138. expect(mockShowToast).toHaveBeenCalledWith(
  139. expect.objectContaining({ title: '库存不足' })
  140. )
  141. // 商品不应被添加
  142. expect(getByTestId('items-count').textContent).toBe('0')
  143. })
  144. it('应该支持同时添加父商品和不同子商品', () => {
  145. const parentGoods: CartItem = {
  146. id: 5001,
  147. parentGoodsId: 0, // 父商品,无父商品
  148. name: '测试父商品',
  149. price: 199.9,
  150. image: 'parent.jpg',
  151. stock: 20,
  152. quantity: 1,
  153. }
  154. const childGoods1: CartItem = {
  155. id: 5002, // 子商品ID1
  156. parentGoodsId: 5001, // 父商品ID
  157. name: '测试父商品 - 规格A',
  158. price: 219.9,
  159. image: 'child1.jpg',
  160. stock: 5,
  161. quantity: 2,
  162. spec: '规格A',
  163. }
  164. const childGoods2: CartItem = {
  165. id: 5003, // 子商品ID2
  166. parentGoodsId: 5001, // 父商品ID
  167. name: '测试父商品 - 规格B',
  168. price: 229.9,
  169. image: 'child2.jpg',
  170. stock: 3,
  171. quantity: 1,
  172. spec: '规格B',
  173. }
  174. const { getByTestId, rerender } = render(
  175. <CartProvider>
  176. <TestComponent action="add" item={parentGoods} />
  177. </CartProvider>
  178. )
  179. expect(getByTestId('items-count').textContent).toBe('1')
  180. // 添加第一个子商品
  181. rerender(
  182. <CartProvider>
  183. <TestComponent action="add" item={childGoods1} />
  184. </CartProvider>
  185. )
  186. expect(getByTestId('items-count').textContent).toBe('2')
  187. // 添加第二个子商品
  188. rerender(
  189. <CartProvider>
  190. <TestComponent action="add" item={childGoods2} />
  191. </CartProvider>
  192. )
  193. expect(getByTestId('items-count').textContent).toBe('3')
  194. expect(getByTestId('item-0-id').textContent).toBe('5001')
  195. expect(getByTestId('item-1-id').textContent).toBe('5002')
  196. expect(getByTestId('item-2-id').textContent).toBe('5003')
  197. })
  198. it('应该支持切换购物车项规格', () => {
  199. // 首先添加一个子商品到购物车
  200. const childGoods: CartItem = {
  201. id: 6001,
  202. parentGoodsId: 6000, // 父商品ID
  203. name: '测试父商品 - 规格A',
  204. price: 99.9,
  205. image: 'child1.jpg',
  206. stock: 10,
  207. quantity: 2,
  208. spec: '规格A',
  209. }
  210. // 创建一个新的测试组件来测试switchSpec
  211. const TestSwitchSpecComponent = ({ cartItemId, newChildGoods }: {
  212. cartItemId?: number,
  213. newChildGoods?: { id: number; name: string; price: number; stock: number; image?: string; spec?: string }
  214. }) => {
  215. const cart = useCart()
  216. React.useEffect(() => {
  217. if (!cart.isLoading && cartItemId && newChildGoods) {
  218. cart.switchSpec(cartItemId, newChildGoods)
  219. }
  220. }, [cart.isLoading, cartItemId, newChildGoods])
  221. return (
  222. <div>
  223. <div data-testid="items-count">{cart.cart.items.length}</div>
  224. {cart.cart.items.map((item, index) => (
  225. <div key={index} data-testid={`item-${index}`}>
  226. <span data-testid={`item-${index}-id`}>{item.id}</span>
  227. <span data-testid={`item-${index}-name`}>{item.name}</span>
  228. {/* spec字段已移除 */}
  229. <span data-testid={`item-${index}-quantity`}>{item.quantity}</span>
  230. <span data-testid={`item-${index}-price`}>{item.price}</span>
  231. </div>
  232. ))}
  233. </div>
  234. )
  235. }
  236. const { getByTestId, rerender } = render(
  237. <CartProvider>
  238. <TestComponent action="add" item={childGoods} />
  239. </CartProvider>
  240. )
  241. expect(getByTestId('items-count').textContent).toBe('1')
  242. expect(getByTestId('item-0-id').textContent).toBe('6001')
  243. expect(getByTestId('item-0-name').textContent).toBe('测试父商品 - 规格A')
  244. // 切换到新规格
  245. const newChildGoods = {
  246. id: 6002,
  247. name: '规格B', // 规格名称
  248. price: 119.9,
  249. stock: 5,
  250. image: 'child2.jpg',
  251. }
  252. rerender(
  253. <CartProvider>
  254. <TestSwitchSpecComponent cartItemId={6001} newChildGoods={newChildGoods} />
  255. </CartProvider>
  256. )
  257. // 验证规格已切换
  258. expect(getByTestId('items-count').textContent).toBe('1')
  259. expect(getByTestId('item-0-id').textContent).toBe('6002') // ID已更新
  260. expect(getByTestId('item-0-name').textContent).toBe('规格B') // 规格名称
  261. expect(getByTestId('item-0-price').textContent).toBe('119.9')
  262. expect(getByTestId('item-0-quantity').textContent).toBe('2') // 数量保持不变
  263. })
  264. it('切换规格时应该验证库存', () => {
  265. const childGoods: CartItem = {
  266. id: 7001,
  267. parentGoodsId: 7000,
  268. name: '测试商品 - 规格A',
  269. price: 50,
  270. image: 'test.jpg',
  271. stock: 10,
  272. quantity: 8, // 当前数量8
  273. spec: '规格A',
  274. }
  275. const TestSwitchSpecComponent = ({ cartItemId, newChildGoods }: {
  276. cartItemId?: number,
  277. newChildGoods?: { id: number; name: string; price: number; stock: number; image?: string; spec?: string }
  278. }) => {
  279. const cart = useCart()
  280. React.useEffect(() => {
  281. if (!cart.isLoading && cartItemId && newChildGoods) {
  282. cart.switchSpec(cartItemId, newChildGoods)
  283. }
  284. }, [cart.isLoading, cartItemId, newChildGoods])
  285. return <div data-testid="toast-called">{mockShowToast.mock.calls.length}</div>
  286. }
  287. // 添加商品到购物车
  288. const { getByTestId, rerender } = render(
  289. <CartProvider>
  290. <TestComponent action="add" item={childGoods} />
  291. </CartProvider>
  292. )
  293. // 尝试切换到库存不足的规格(库存只有5,但当前数量是8)
  294. const newChildGoods = {
  295. id: 7002,
  296. name: '测试商品 - 规格B',
  297. price: 60,
  298. stock: 5, // 库存不足
  299. image: 'test2.jpg',
  300. spec: '规格B'
  301. }
  302. rerender(
  303. <CartProvider>
  304. <TestSwitchSpecComponent cartItemId={7001} newChildGoods={newChildGoods} />
  305. </CartProvider>
  306. )
  307. // 应该显示库存不足提示
  308. expect(mockShowToast).toHaveBeenCalledWith(
  309. expect.objectContaining({ title: expect.stringContaining('库存不足') })
  310. )
  311. })
  312. it('单规格商品不应该支持切换规格', () => {
  313. const singleSpecGoods: CartItem = {
  314. id: 8001,
  315. parentGoodsId: 0, // 单规格商品
  316. name: '单规格商品',
  317. price: 30,
  318. image: 'single.jpg',
  319. stock: 10,
  320. quantity: 1,
  321. }
  322. const TestSwitchSpecComponent = ({ cartItemId, newChildGoods }: {
  323. cartItemId?: number,
  324. newChildGoods?: { id: number; name: string; price: number; stock: number; image?: string; spec?: string }
  325. }) => {
  326. const cart = useCart()
  327. React.useEffect(() => {
  328. if (!cart.isLoading && cartItemId && newChildGoods) {
  329. cart.switchSpec(cartItemId, newChildGoods)
  330. }
  331. }, [cart.isLoading, cartItemId, newChildGoods])
  332. return <div>Test</div>
  333. }
  334. // 添加单规格商品
  335. const { rerender } = render(
  336. <CartProvider>
  337. <TestComponent action="add" item={singleSpecGoods} />
  338. </CartProvider>
  339. )
  340. const newChildGoods = {
  341. id: 8002,
  342. name: '新规格',
  343. price: 40,
  344. stock: 5,
  345. spec: '新规格'
  346. }
  347. rerender(
  348. <CartProvider>
  349. <TestSwitchSpecComponent cartItemId={8001} newChildGoods={newChildGoods} />
  350. </CartProvider>
  351. )
  352. // 应该显示不支持切换的提示
  353. expect(mockShowToast).toHaveBeenCalledWith(
  354. expect.objectContaining({ title: '该商品不支持切换规格' })
  355. )
  356. })
  357. })