CartContext.test.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. <span data-testid={`item-${index}-spec`}>{item.spec || ''}</span>
  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. spec: '红色/M', // 规格信息
  72. }
  73. const { getByTestId } = render(
  74. <CartProvider>
  75. <TestComponent action="add" item={childGoods} />
  76. </CartProvider>
  77. )
  78. expect(getByTestId('items-count').textContent).toBe('1')
  79. expect(getByTestId('item-0-id').textContent).toBe('2001')
  80. expect(getByTestId('item-0-name').textContent).toBe('测试父商品 - 红色/M')
  81. expect(getByTestId('item-0-spec').textContent).toBe('红色/M')
  82. expect(mockSetStorageSync).toHaveBeenCalled()
  83. })
  84. it('应该支持添加同一子商品多次(数量累加)', () => {
  85. const childGoods1: CartItem = {
  86. id: 3001,
  87. parentGoodsId: 3000, // 父商品ID
  88. name: '测试商品 - 蓝色/L',
  89. price: 89.9,
  90. image: 'goods.jpg',
  91. stock: 10,
  92. quantity: 1,
  93. spec: '蓝色/L',
  94. }
  95. const childGoods2: CartItem = {
  96. id: 3001, // 同一子商品ID
  97. parentGoodsId: 3000, // 父商品ID
  98. name: '测试商品 - 蓝色/L',
  99. price: 89.9,
  100. image: 'goods.jpg',
  101. stock: 10,
  102. quantity: 3,
  103. spec: '蓝色/L',
  104. }
  105. const { getByTestId, rerender } = render(
  106. <CartProvider>
  107. <TestComponent action="add" item={childGoods1} />
  108. </CartProvider>
  109. )
  110. expect(getByTestId('items-count').textContent).toBe('1')
  111. console.log('Item 0 id:', getByTestId('item-0-id').textContent)
  112. console.log('Item 0 name:', getByTestId('item-0-name').textContent)
  113. console.log('Item 0 spec:', getByTestId('item-0-spec').textContent)
  114. const quantityElement = getByTestId('item-0-quantity')
  115. console.log('Quantity element text:', quantityElement.textContent)
  116. // 修复:检查数量是否正确,应该是1而不是库存值10
  117. expect(parseInt(quantityElement.textContent || '0')).toBe(1)
  118. // 重新渲染添加更多数量
  119. rerender(
  120. <CartProvider>
  121. <TestComponent action="add" item={childGoods2} />
  122. </CartProvider>
  123. )
  124. expect(getByTestId('item-0-quantity').textContent).toBe('4') // 1 + 3
  125. })
  126. it('应该限制数量不超过库存', () => {
  127. const childGoods: CartItem = {
  128. id: 4001,
  129. parentGoodsId: 4000, // 父商品ID
  130. name: '测试商品 - 黑色/XL',
  131. price: 129.9,
  132. image: 'goods.jpg',
  133. stock: 2, // 库存只有2
  134. quantity: 3, // 尝试购买3个
  135. spec: '黑色/XL',
  136. }
  137. const { getByTestId } = render(
  138. <CartProvider>
  139. <TestComponent action="add" item={childGoods} />
  140. </CartProvider>
  141. )
  142. // 应该显示库存不足提示
  143. expect(mockShowToast).toHaveBeenCalledWith(
  144. expect.objectContaining({ title: '库存不足' })
  145. )
  146. // 商品不应被添加
  147. expect(getByTestId('items-count').textContent).toBe('0')
  148. })
  149. it('应该支持同时添加父商品和不同子商品', () => {
  150. const parentGoods: CartItem = {
  151. id: 5001,
  152. parentGoodsId: 0, // 父商品,无父商品
  153. name: '测试父商品',
  154. price: 199.9,
  155. image: 'parent.jpg',
  156. stock: 20,
  157. quantity: 1,
  158. }
  159. const childGoods1: CartItem = {
  160. id: 5002, // 子商品ID1
  161. parentGoodsId: 5001, // 父商品ID
  162. name: '测试父商品 - 规格A',
  163. price: 219.9,
  164. image: 'child1.jpg',
  165. stock: 5,
  166. quantity: 2,
  167. spec: '规格A',
  168. }
  169. const childGoods2: CartItem = {
  170. id: 5003, // 子商品ID2
  171. parentGoodsId: 5001, // 父商品ID
  172. name: '测试父商品 - 规格B',
  173. price: 229.9,
  174. image: 'child2.jpg',
  175. stock: 3,
  176. quantity: 1,
  177. spec: '规格B',
  178. }
  179. const { getByTestId, rerender } = render(
  180. <CartProvider>
  181. <TestComponent action="add" item={parentGoods} />
  182. </CartProvider>
  183. )
  184. expect(getByTestId('items-count').textContent).toBe('1')
  185. // 添加第一个子商品
  186. rerender(
  187. <CartProvider>
  188. <TestComponent action="add" item={childGoods1} />
  189. </CartProvider>
  190. )
  191. expect(getByTestId('items-count').textContent).toBe('2')
  192. // 添加第二个子商品
  193. rerender(
  194. <CartProvider>
  195. <TestComponent action="add" item={childGoods2} />
  196. </CartProvider>
  197. )
  198. expect(getByTestId('items-count').textContent).toBe('3')
  199. expect(getByTestId('item-0-id').textContent).toBe('5001')
  200. expect(getByTestId('item-1-id').textContent).toBe('5002')
  201. expect(getByTestId('item-2-id').textContent).toBe('5003')
  202. })
  203. it('应该支持切换购物车项规格', () => {
  204. // 首先添加一个子商品到购物车
  205. const childGoods: CartItem = {
  206. id: 6001,
  207. parentGoodsId: 6000, // 父商品ID
  208. name: '测试父商品 - 规格A',
  209. price: 99.9,
  210. image: 'child1.jpg',
  211. stock: 10,
  212. quantity: 2,
  213. spec: '规格A',
  214. }
  215. // 创建一个新的测试组件来测试switchSpec
  216. const TestSwitchSpecComponent = ({ cartItemId, newChildGoods }: {
  217. cartItemId?: number,
  218. newChildGoods?: { id: number; name: string; price: number; stock: number; image?: string; spec?: string }
  219. }) => {
  220. const cart = useCart()
  221. React.useEffect(() => {
  222. if (!cart.isLoading && cartItemId && newChildGoods) {
  223. cart.switchSpec(cartItemId, newChildGoods)
  224. }
  225. }, [cart.isLoading, cartItemId, newChildGoods])
  226. return (
  227. <div>
  228. <div data-testid="items-count">{cart.cart.items.length}</div>
  229. {cart.cart.items.map((item, index) => (
  230. <div key={index} data-testid={`item-${index}`}>
  231. <span data-testid={`item-${index}-id`}>{item.id}</span>
  232. <span data-testid={`item-${index}-name`}>{item.name}</span>
  233. <span data-testid={`item-${index}-spec`}>{item.spec || ''}</span>
  234. <span data-testid={`item-${index}-quantity`}>{item.quantity}</span>
  235. <span data-testid={`item-${index}-price`}>{item.price}</span>
  236. </div>
  237. ))}
  238. </div>
  239. )
  240. }
  241. const { getByTestId, rerender } = render(
  242. <CartProvider>
  243. <TestComponent action="add" item={childGoods} />
  244. </CartProvider>
  245. )
  246. expect(getByTestId('items-count').textContent).toBe('1')
  247. expect(getByTestId('item-0-id').textContent).toBe('6001')
  248. expect(getByTestId('item-0-name').textContent).toBe('测试父商品 - 规格A')
  249. // 切换到新规格
  250. const newChildGoods = {
  251. id: 6002,
  252. name: '测试父商品 - 规格B',
  253. price: 119.9,
  254. stock: 5,
  255. image: 'child2.jpg',
  256. spec: '规格B'
  257. }
  258. rerender(
  259. <CartProvider>
  260. <TestSwitchSpecComponent cartItemId={6001} newChildGoods={newChildGoods} />
  261. </CartProvider>
  262. )
  263. // 验证规格已切换
  264. expect(getByTestId('items-count').textContent).toBe('1')
  265. expect(getByTestId('item-0-id').textContent).toBe('6002') // ID已更新
  266. expect(getByTestId('item-0-name').textContent).toBe('测试父商品 - 规格B')
  267. expect(getByTestId('item-0-spec').textContent).toBe('规格B')
  268. expect(getByTestId('item-0-price').textContent).toBe('119.9')
  269. expect(getByTestId('item-0-quantity').textContent).toBe('2') // 数量保持不变
  270. })
  271. it('切换规格时应该验证库存', () => {
  272. const childGoods: CartItem = {
  273. id: 7001,
  274. parentGoodsId: 7000,
  275. name: '测试商品 - 规格A',
  276. price: 50,
  277. image: 'test.jpg',
  278. stock: 10,
  279. quantity: 8, // 当前数量8
  280. spec: '规格A',
  281. }
  282. const TestSwitchSpecComponent = ({ cartItemId, newChildGoods }: {
  283. cartItemId?: number,
  284. newChildGoods?: { id: number; name: string; price: number; stock: number; image?: string; spec?: string }
  285. }) => {
  286. const cart = useCart()
  287. React.useEffect(() => {
  288. if (!cart.isLoading && cartItemId && newChildGoods) {
  289. cart.switchSpec(cartItemId, newChildGoods)
  290. }
  291. }, [cart.isLoading, cartItemId, newChildGoods])
  292. return <div data-testid="toast-called">{mockShowToast.mock.calls.length}</div>
  293. }
  294. // 添加商品到购物车
  295. const { getByTestId, rerender } = render(
  296. <CartProvider>
  297. <TestComponent action="add" item={childGoods} />
  298. </CartProvider>
  299. )
  300. // 尝试切换到库存不足的规格(库存只有5,但当前数量是8)
  301. const newChildGoods = {
  302. id: 7002,
  303. name: '测试商品 - 规格B',
  304. price: 60,
  305. stock: 5, // 库存不足
  306. image: 'test2.jpg',
  307. spec: '规格B'
  308. }
  309. rerender(
  310. <CartProvider>
  311. <TestSwitchSpecComponent cartItemId={7001} newChildGoods={newChildGoods} />
  312. </CartProvider>
  313. )
  314. // 应该显示库存不足提示
  315. expect(mockShowToast).toHaveBeenCalledWith(
  316. expect.objectContaining({ title: expect.stringContaining('库存不足') })
  317. )
  318. })
  319. it('单规格商品不应该支持切换规格', () => {
  320. const singleSpecGoods: CartItem = {
  321. id: 8001,
  322. parentGoodsId: 0, // 单规格商品
  323. name: '单规格商品',
  324. price: 30,
  325. image: 'single.jpg',
  326. stock: 10,
  327. quantity: 1,
  328. }
  329. const TestSwitchSpecComponent = ({ cartItemId, newChildGoods }: {
  330. cartItemId?: number,
  331. newChildGoods?: { id: number; name: string; price: number; stock: number; image?: string; spec?: string }
  332. }) => {
  333. const cart = useCart()
  334. React.useEffect(() => {
  335. if (!cart.isLoading && cartItemId && newChildGoods) {
  336. cart.switchSpec(cartItemId, newChildGoods)
  337. }
  338. }, [cart.isLoading, cartItemId, newChildGoods])
  339. return <div>Test</div>
  340. }
  341. // 添加单规格商品
  342. const { rerender } = render(
  343. <CartProvider>
  344. <TestComponent action="add" item={singleSpecGoods} />
  345. </CartProvider>
  346. )
  347. const newChildGoods = {
  348. id: 8002,
  349. name: '新规格',
  350. price: 40,
  351. stock: 5,
  352. spec: '新规格'
  353. }
  354. rerender(
  355. <CartProvider>
  356. <TestSwitchSpecComponent cartItemId={8001} newChildGoods={newChildGoods} />
  357. </CartProvider>
  358. )
  359. // 应该显示不支持切换的提示
  360. expect(mockShowToast).toHaveBeenCalledWith(
  361. expect.objectContaining({ title: '该商品不支持切换规格' })
  362. )
  363. })
  364. })