test: 套装方案对比功能的单元测试和e2e测试
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Successful in 3m0s
All checks were successful
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 13s
Test / e2e-test (push) Successful in 3m0s
单元测试(21个): 套装配置验证、油名解析、折扣率计算、配方匹配逻辑 e2e测试(16个): 页面加载、权限控制、套装卡片、配方表格、横向对比、导出按钮 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
253
frontend/src/__tests__/kitCost.test.js
Normal file
253
frontend/src/__tests__/kitCost.test.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import prodData from './fixtures/production-data.json'
|
||||
import { KITS } from '../config/kits'
|
||||
|
||||
const oils = prodData.oils
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Replicate kit cost calculation logic from useKitCost.js (pure functions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function resolveOilName(kitOilName) {
|
||||
if (oils[kitOilName]) return kitOilName
|
||||
const match = Object.keys(oils).find(n => n.endsWith(kitOilName) && n !== kitOilName)
|
||||
return match || kitOilName
|
||||
}
|
||||
|
||||
function calcKitPerDrop(kit) {
|
||||
const resolved = kit.oils.map(resolveOilName)
|
||||
const bc = kit.bottleCount || {}
|
||||
let totalBottlePrice = 0
|
||||
const oilBottlePrices = {}
|
||||
for (const name of resolved) {
|
||||
const meta = oils[name]
|
||||
const count = bc[name] || 1
|
||||
const bp = meta ? meta.bottlePrice * count : 0
|
||||
oilBottlePrices[name] = bp
|
||||
totalBottlePrice += bp
|
||||
}
|
||||
if (totalBottlePrice === 0) return {}
|
||||
|
||||
const totalValue = totalBottlePrice + (kit.accessoryValue || 0)
|
||||
const discountRate = Math.min(kit.price / totalValue, 1)
|
||||
const perDrop = {}
|
||||
for (const name of resolved) {
|
||||
const meta = oils[name]
|
||||
const count = bc[name] || 1
|
||||
const bp = oilBottlePrices[name]
|
||||
const kitCostForOil = bp * discountRate
|
||||
const totalDrops = meta ? meta.dropCount * count : 1
|
||||
perDrop[name] = totalDrops > 0 ? kitCostForOil / totalDrops : 0
|
||||
}
|
||||
return perDrop
|
||||
}
|
||||
|
||||
function canMakeRecipe(kit, recipe) {
|
||||
const resolvedSet = new Set(kit.oils.map(resolveOilName))
|
||||
return recipe.ingredients.every(ing => resolvedSet.has(ing.oil))
|
||||
}
|
||||
|
||||
function calcRecipeCostWithKit(kitPerDrop, recipe) {
|
||||
return recipe.ingredients.reduce((sum, ing) => {
|
||||
return sum + (kitPerDrop[ing.oil] || 0) * ing.drops
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function calcOriginalCost(recipe) {
|
||||
return recipe.ingredients.reduce((sum, ing) => {
|
||||
const meta = oils[ing.oil]
|
||||
if (!meta || !meta.dropCount) return sum
|
||||
return sum + (meta.bottlePrice / meta.dropCount) * ing.drops
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kit Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Kit Configuration', () => {
|
||||
it('has 4 kits defined', () => {
|
||||
expect(KITS).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('each kit has required fields', () => {
|
||||
for (const kit of KITS) {
|
||||
expect(kit).toHaveProperty('id')
|
||||
expect(kit).toHaveProperty('name')
|
||||
expect(kit).toHaveProperty('price')
|
||||
expect(kit).toHaveProperty('oils')
|
||||
expect(kit.price).toBeGreaterThan(0)
|
||||
expect(kit.oils.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('all kits include coconut oil', () => {
|
||||
for (const kit of KITS) {
|
||||
expect(kit.oils).toContain('椰子油')
|
||||
}
|
||||
})
|
||||
|
||||
it('kit ids are unique', () => {
|
||||
const ids = KITS.map(k => k.id)
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
})
|
||||
|
||||
it('芳香调理 has 9 oils at ¥1575', () => {
|
||||
const kit = KITS.find(k => k.id === 'aroma')
|
||||
expect(kit.price).toBe(1575)
|
||||
expect(kit.oils).toHaveLength(9)
|
||||
})
|
||||
|
||||
it('家庭医生 has 11 oils at ¥2250', () => {
|
||||
const kit = KITS.find(k => k.id === 'family')
|
||||
expect(kit.price).toBe(2250)
|
||||
expect(kit.oils).toHaveLength(11)
|
||||
})
|
||||
|
||||
it('居家呵护 has 22 oils at ¥3988', () => {
|
||||
const kit = KITS.find(k => k.id === 'home3988')
|
||||
expect(kit.price).toBe(3988)
|
||||
expect(kit.oils).toHaveLength(22)
|
||||
})
|
||||
|
||||
it('全精油 has 80 oils at ¥17700 with bottleCount for coconut oil', () => {
|
||||
const kit = KITS.find(k => k.id === 'full')
|
||||
expect(kit.price).toBe(17700)
|
||||
expect(kit.oils.length).toBe(80)
|
||||
expect(kit.bottleCount).toBeDefined()
|
||||
expect(kit.bottleCount['椰子油']).toBeCloseTo(2.57, 2)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Oil Name Resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Oil Name Resolution', () => {
|
||||
it('resolves exact match', () => {
|
||||
expect(resolveOilName('薰衣草')).toBe('薰衣草')
|
||||
expect(resolveOilName('乳香')).toBe('乳香')
|
||||
})
|
||||
|
||||
it('resolves 牛至 to 西班牙牛至 via endsWith', () => {
|
||||
expect(resolveOilName('牛至')).toBe('西班牙牛至')
|
||||
})
|
||||
|
||||
it('does NOT resolve 牛至 to 牛至呵护', () => {
|
||||
expect(resolveOilName('牛至')).not.toBe('牛至呵护')
|
||||
})
|
||||
|
||||
it('returns original name for unknown oil', () => {
|
||||
expect(resolveOilName('不存在的油')).toBe('不存在的油')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kit Cost Calculation
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Kit Cost Calculation', () => {
|
||||
it('discount rate is always <= 1 (kit never more expensive than buying individually)', () => {
|
||||
for (const kit of KITS) {
|
||||
const perDrop = calcKitPerDrop(kit)
|
||||
for (const [name, ppd] of Object.entries(perDrop)) {
|
||||
const meta = oils[name]
|
||||
if (!meta || !meta.dropCount) continue
|
||||
const originalPpd = meta.bottlePrice / meta.dropCount
|
||||
expect(ppd).toBeLessThanOrEqual(originalPpd + 0.001) // tiny float tolerance
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('家庭医生 discount is ~32-33%', () => {
|
||||
const kit = KITS.find(k => k.id === 'family')
|
||||
const bc = kit.bottleCount || {}
|
||||
let totalBp = 0
|
||||
for (const name of kit.oils) {
|
||||
const resolved = resolveOilName(name)
|
||||
const meta = oils[resolved]
|
||||
totalBp += meta ? meta.bottlePrice * (bc[resolved] || 1) : 0
|
||||
}
|
||||
const totalValue = totalBp + (kit.accessoryValue || 0)
|
||||
const discount = 1 - kit.price / totalValue
|
||||
expect(discount).toBeGreaterThan(0.30)
|
||||
expect(discount).toBeLessThan(0.40)
|
||||
})
|
||||
|
||||
it('kit cost for a recipe is less than original cost', () => {
|
||||
const kit = KITS.find(k => k.id === 'family')
|
||||
const perDrop = calcKitPerDrop(kit)
|
||||
// 灰指甲: 西班牙牛至 + 椰子油
|
||||
const recipe = prodData.recipes.find(r => r.name === '灰指甲')
|
||||
if (recipe && canMakeRecipe(kit, recipe)) {
|
||||
const kitCost = calcRecipeCostWithKit(perDrop, recipe)
|
||||
const origCost = calcOriginalCost(recipe)
|
||||
expect(kitCost).toBeLessThan(origCost)
|
||||
expect(kitCost).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('全精油 bottleCount 2.57 makes coconut oil cheaper per drop than without multiplier', () => {
|
||||
const fullKit = KITS.find(k => k.id === 'full')
|
||||
const perDrop = calcKitPerDrop(fullKit)
|
||||
// With 2.57 bottles, per-drop cost should be roughly 1/2.57 of single-bottle kit cost
|
||||
// at the same discount rate. Just verify it's significantly less than original per-drop.
|
||||
const origPpd = oils['椰子油'].bottlePrice / oils['椰子油'].dropCount
|
||||
expect(perDrop['椰子油']).toBeLessThan(origPpd)
|
||||
expect(perDrop['椰子油']).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('accessoryValue reduces effective oil cost', () => {
|
||||
const kit = KITS.find(k => k.id === 'family')
|
||||
// Without accessory: rate = price / totalBp
|
||||
// With accessory: rate = price / (totalBp + accessoryValue) < previous rate
|
||||
expect(kit.accessoryValue).toBeGreaterThan(0)
|
||||
|
||||
const bc = kit.bottleCount || {}
|
||||
let totalBp = 0
|
||||
for (const name of kit.oils) {
|
||||
const resolved = resolveOilName(name)
|
||||
const meta = oils[resolved]
|
||||
totalBp += meta ? meta.bottlePrice * (bc[resolved] || 1) : 0
|
||||
}
|
||||
const rateWithAcc = kit.price / (totalBp + kit.accessoryValue)
|
||||
const rateWithoutAcc = kit.price / totalBp
|
||||
expect(rateWithAcc).toBeLessThan(rateWithoutAcc)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recipe Matching
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Recipe Matching', () => {
|
||||
it('larger kits can make at least as many recipes as smaller ones', () => {
|
||||
const counts = KITS.map(kit => {
|
||||
const matched = prodData.recipes.filter(r => canMakeRecipe(kit, r))
|
||||
return { id: kit.id, count: matched.length, oilCount: kit.oils.length }
|
||||
}).sort((a, b) => a.oilCount - b.oilCount)
|
||||
|
||||
for (let i = 1; i < counts.length; i++) {
|
||||
expect(counts[i].count).toBeGreaterThanOrEqual(counts[i - 1].count)
|
||||
}
|
||||
})
|
||||
|
||||
it('全精油 can make the most recipes', () => {
|
||||
const fullKit = KITS.find(k => k.id === 'full')
|
||||
const fullCount = prodData.recipes.filter(r => canMakeRecipe(fullKit, r)).length
|
||||
|
||||
for (const kit of KITS) {
|
||||
if (kit.id === 'full') continue
|
||||
const count = prodData.recipes.filter(r => canMakeRecipe(kit, r)).length
|
||||
expect(fullCount).toBeGreaterThanOrEqual(count)
|
||||
}
|
||||
})
|
||||
|
||||
it('灰指甲 (牛至+椰子油) can be made by 家庭医生', () => {
|
||||
const kit = KITS.find(k => k.id === 'family')
|
||||
const recipe = prodData.recipes.find(r => r.name === '灰指甲')
|
||||
expect(canMakeRecipe(kit, recipe)).toBe(true)
|
||||
})
|
||||
|
||||
it('recipe requiring 永久花 cannot be made by 芳香调理', () => {
|
||||
const kit = KITS.find(k => k.id === 'aroma')
|
||||
const recipe = prodData.recipes.find(r => r.name === '小v脸') // has 永久花
|
||||
expect(canMakeRecipe(kit, recipe)).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user