campus-errand/miniapp/stores/cart.test.js
2026-03-12 18:12:10 +08:00

188 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Property 7: 购物车数量与价格不变量
*
* 对于任意购物车状态:
* - 总数量 = 所有菜品数量之和
* - 总价 = 所有菜品的(数量 × 单价)之和
* - 添加菜品使数量 +1
* - 减少菜品使数量 -1减至 0 时移除
*/
import { describe, it, expect } from 'vitest'
import * as fc from 'fast-check'
// ==================== 纯函数购物车逻辑(与 store 一致) ====================
/**
* 计算购物车总数量
* @param {Array} items - [{ dish, quantity }]
*/
function calcTotalCount(items) {
return items.reduce((sum, item) => sum + item.quantity, 0)
}
/**
* 计算购物车总价
* @param {Array} items - [{ dish: { price }, quantity }]
*/
function calcTotalPrice(items) {
return items.reduce((sum, item) => sum + item.quantity * item.dish.price, 0)
}
/**
* 添加菜品,数量 +1
*/
function addItem(items, dish) {
const copy = items.map(i => ({ dish: { ...i.dish }, quantity: i.quantity }))
const existing = copy.find(i => i.dish.id === dish.id)
if (existing) {
existing.quantity += 1
} else {
copy.push({ dish: { ...dish }, quantity: 1 })
}
return copy
}
/**
* 减少菜品,数量 -1减至 0 时移除
*/
function removeItem(items, dishId) {
const copy = items.map(i => ({ dish: { ...i.dish }, quantity: i.quantity }))
const index = copy.findIndex(i => i.dish.id === dishId)
if (index === -1) return copy
copy[index].quantity -= 1
if (copy[index].quantity <= 0) {
copy.splice(index, 1)
}
return copy
}
// ==================== 生成器 ====================
/** 生成合法菜品对象 */
const dishArb = fc.record({
id: fc.integer({ min: 1, max: 1000 }),
name: fc.string({ minLength: 1, maxLength: 10 }),
photo: fc.constant('test.png'),
price: fc.float({ min: Math.fround(0.01), max: Math.fround(999), noNaN: true }).map(v => Math.round(v * 100) / 100)
})
/** 生成购物车操作序列 */
const cartActionArb = fc.oneof(
dishArb.map(dish => ({ type: 'add', dish })),
fc.integer({ min: 1, max: 1000 }).map(id => ({ type: 'remove', dishId: id }))
)
// ==================== 属性测试 ====================
describe('Feature: login-and-homepage, Property 7: 购物车数量与价格不变量', () => {
/**
*
* 总数量始终等于所有菜品数量之和
*/
it('总数量等于所有菜品数量之和', () => {
fc.assert(
fc.property(
fc.array(cartActionArb, { minLength: 1, maxLength: 50 }),
(actions) => {
let items = []
for (const action of actions) {
if (action.type === 'add') {
items = addItem(items, action.dish)
} else {
items = removeItem(items, action.dishId)
}
}
const expectedCount = items.reduce((s, i) => s + i.quantity, 0)
expect(calcTotalCount(items)).toBe(expectedCount)
}
),
{ numRuns: 200 }
)
})
/**
*
* 总价始终等于所有菜品的(数量 × 单价)之和
*/
it('总价等于所有菜品的数量乘以单价之和', () => {
fc.assert(
fc.property(
fc.array(cartActionArb, { minLength: 1, maxLength: 50 }),
(actions) => {
let items = []
for (const action of actions) {
if (action.type === 'add') {
items = addItem(items, action.dish)
} else {
items = removeItem(items, action.dishId)
}
}
const expectedPrice = items.reduce((s, i) => s + i.quantity * i.dish.price, 0)
// 浮点精度容差
expect(Math.abs(calcTotalPrice(items) - expectedPrice)).toBeLessThan(0.001)
}
),
{ numRuns: 200 }
)
})
/**
*
* 添加菜品使该菜品数量 +1
*/
it('添加菜品使数量加1', () => {
fc.assert(
fc.property(
fc.array(cartActionArb, { maxLength: 20 }),
dishArb,
(setupActions, dish) => {
let items = []
for (const action of setupActions) {
if (action.type === 'add') {
items = addItem(items, action.dish)
} else {
items = removeItem(items, action.dishId)
}
}
const beforeQty = items.find(i => i.dish.id === dish.id)?.quantity || 0
const afterItems = addItem(items, dish)
const afterQty = afterItems.find(i => i.dish.id === dish.id)?.quantity || 0
expect(afterQty).toBe(beforeQty + 1)
}
),
{ numRuns: 200 }
)
})
/**
*
* 减少菜品使数量 -1减至 0 时移除
*/
it('减少菜品使数量减1且减至0时移除', () => {
fc.assert(
fc.property(
dishArb,
fc.integer({ min: 1, max: 10 }),
(dish, addCount) => {
// 先添加若干次
let items = []
for (let i = 0; i < addCount; i++) {
items = addItem(items, dish)
}
expect(items.find(i => i.dish.id === dish.id)?.quantity).toBe(addCount)
// 减少一次
const afterItems = removeItem(items, dish.id)
if (addCount === 1) {
// 减至 0应被移除
expect(afterItems.find(i => i.dish.id === dish.id)).toBeUndefined()
} else {
expect(afterItems.find(i => i.dish.id === dish.id)?.quantity).toBe(addCount - 1)
}
}
),
{ numRuns: 200 }
)
})
})