202 lines
7.1 KiB
TypeScript
202 lines
7.1 KiB
TypeScript
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<Product> = 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<SpecData> = 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<CartItem> = 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> = {}): 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)
|
|
})
|
|
})
|