import { describe, it, expect, beforeEach } from 'vitest' import * as fc from 'fast-check' import { setActivePinia, createPinia } from 'pinia' import { useCartStore } from './cart' import type { CartItem } from '../types' import type { Product, SpecData } from '../types/product' // --- Arbitraries --- const productArb: fc.Arbitrary = fc.record({ id: fc.integer({ min: 1, max: 100000 }), name: fc.string({ minLength: 1, maxLength: 20 }), basePrice: fc.float({ min: 0, max: 100000, noNaN: true }), styleNo: fc.string({ minLength: 1, maxLength: 10 }), stock: fc.integer({ min: 0, max: 10000 }), totalStock: fc.integer({ min: 1, max: 10000 }), loss: fc.float({ min: 0, max: 10, noNaN: true }), laborCost: fc.float({ min: 0, max: 10000, noNaN: true }), categoryId: fc.integer({ min: 1, max: 100 }), bannerImages: fc.array(fc.string(), { minLength: 0, maxLength: 3 }), bannerVideo: fc.option(fc.string(), { nil: undefined }), status: fc.constantFrom('on' as const, 'off' as const), createdAt: fc.constant('2025-01-01'), updatedAt: fc.constant('2025-01-01'), }) const specDataArb: fc.Arbitrary = fc.record({ id: fc.integer({ min: 1, max: 100000 }), productId: fc.integer({ min: 1, max: 100000 }), modelName: fc.string({ minLength: 1, maxLength: 20 }), fineness: fc.string({ minLength: 1, maxLength: 10 }), mainStone: fc.string({ minLength: 1, maxLength: 10 }), ringSize: fc.string({ minLength: 1, maxLength: 5 }), goldTotalWeight: fc.float({ min: 0, max: 1000, noNaN: true }), goldNetWeight: fc.float({ min: 0, max: 1000, noNaN: true }), loss: fc.float({ min: 0, max: 10, noNaN: true }), goldLoss: fc.float({ min: 0, max: 100, noNaN: true }), goldPrice: fc.float({ min: 0, max: 10000, noNaN: true }), goldValue: fc.float({ min: 0, max: 100000, noNaN: true }), mainStoneCount: fc.integer({ min: 0, max: 100 }), mainStoneWeight: fc.float({ min: 0, max: 100, noNaN: true }), mainStoneUnitPrice: fc.float({ min: 0, max: 100000, noNaN: true }), mainStoneAmount: fc.float({ min: 0, max: 100000, noNaN: true }), sideStoneCount: fc.integer({ min: 0, max: 100 }), sideStoneWeight: fc.float({ min: 0, max: 100, noNaN: true }), sideStoneUnitPrice: fc.float({ min: 0, max: 100000, noNaN: true }), sideStoneAmount: fc.float({ min: 0, max: 100000, noNaN: true }), accessoryAmount: fc.float({ min: 0, max: 10000, noNaN: true }), processingFee: fc.float({ min: 0, max: 10000, noNaN: true }), settingFee: fc.float({ min: 0, max: 10000, noNaN: true }), totalLaborCost: fc.float({ min: 0, max: 10000, noNaN: true }), totalPrice: fc.float({ min: 0, max: 1000000, noNaN: true }), }) const cartItemArb: fc.Arbitrary = fc.record({ id: fc.integer({ min: 1, max: 100000 }), userId: fc.integer({ min: 1, max: 10000 }), productId: fc.integer({ min: 1, max: 100000 }), specDataId: fc.integer({ min: 1, max: 100000 }), quantity: fc.integer({ min: 1, max: 100 }), product: productArb, specData: specDataArb, checked: fc.boolean(), }) // Feature: jewelry-mall, Property 1: 购物车添加商品不变量 // **Validates: Requirements 2.1** describe('Property 1: 购物车添加商品不变量', () => { it('添加商品后购物车应包含该项且项数增加 1', () => { fc.assert( fc.property(cartItemArb, (newItem) => { setActivePinia(createPinia()) const store = useCartStore() const countBefore = store.items.length store.addToCart(newItem) expect(store.items.length).toBe(countBefore + 1) expect(store.items).toContainEqual(newItem) }), { numRuns: 100 }, ) }) }) // Feature: jewelry-mall, Property 2: 购物车总金额计算正确性 // **Validates: Requirements 2.3** describe('Property 2: 购物车总金额计算正确性', () => { it('总金额应等于所有已勾选商品的单价乘以数量之和', () => { fc.assert( fc.property(fc.array(cartItemArb, { minLength: 0, maxLength: 20 }), (items) => { setActivePinia(createPinia()) const store = useCartStore() for (const item of items) { store.addToCart(item) } const expectedTotal = items .filter((item) => item.checked) .reduce((sum, item) => sum + item.specData.totalPrice * item.quantity, 0) expect(store.totalAmount).toBeCloseTo(expectedTotal, 5) }), { numRuns: 100 }, ) }) }) // 购物车单元测试 // Requirements: 2.1-2.4 describe('购物车单元测试', () => { beforeEach(() => { setActivePinia(createPinia()) }) function makeCartItem(overrides: Partial = {}): CartItem { return { id: 1, userId: 1, productId: 1, specDataId: 1, quantity: 1, product: { id: 1, name: '测试商品', basePrice: 100, styleNo: 'S001', stock: 10, totalStock: 100, loss: 0.05, laborCost: 50, categoryId: 1, bannerImages: [], status: 'on', createdAt: '2025-01-01', updatedAt: '2025-01-01', }, specData: { id: 1, productId: 1, modelName: 'M1', fineness: '18K', mainStone: '钻石', ringSize: '12', goldTotalWeight: 5, goldNetWeight: 4.5, loss: 0.05, goldLoss: 0.25, goldPrice: 500, goldValue: 2250, mainStoneCount: 1, mainStoneWeight: 0.5, mainStoneUnitPrice: 10000, mainStoneAmount: 5000, sideStoneCount: 10, sideStoneWeight: 0.3, sideStoneUnitPrice: 500, sideStoneAmount: 150, accessoryAmount: 100, processingFee: 200, settingFee: 300, totalLaborCost: 500, totalPrice: 8000, }, checked: false, ...overrides, } } it('空购物车的总金额为 0', () => { const store = useCartStore() expect(store.totalAmount).toBe(0) expect(store.checkedItems).toHaveLength(0) }) it('空购物车移除商品不报错', () => { const store = useCartStore() expect(() => store.removeFromCart(999)).not.toThrow() }) it('重复添加同一商品 ID 会产生多条记录', () => { const store = useCartStore() const item = makeCartItem() store.addToCart(item) store.addToCart({ ...item }) expect(store.items).toHaveLength(2) }) it('全选后所有商品被勾选', () => { const store = useCartStore() store.addToCart(makeCartItem({ id: 1, checked: false })) store.addToCart(makeCartItem({ id: 2, checked: false })) store.toggleCheckAll() expect(store.items.every((i) => i.checked)).toBe(true) }) it('全选后再次全选取消所有勾选', () => { const store = useCartStore() store.addToCart(makeCartItem({ id: 1, checked: true })) store.addToCart(makeCartItem({ id: 2, checked: true })) store.toggleCheckAll() expect(store.items.every((i) => !i.checked)).toBe(true) }) it('更新数量后数量正确', () => { const store = useCartStore() store.addToCart(makeCartItem({ id: 1, quantity: 1 })) store.updateQuantity(1, 5) expect(store.items[0].quantity).toBe(5) }) it('勾选商品后总金额正确计算', () => { const store = useCartStore() store.addToCart(makeCartItem({ id: 1, checked: true, quantity: 2 })) // totalPrice = 8000, quantity = 2 => 16000 expect(store.totalAmount).toBe(16000) }) })