Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 3m8s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
254 lines
9.0 KiB
JavaScript
254 lines
9.0 KiB
JavaScript
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 23 oils at ¥3988', () => {
|
|
const kit = KITS.find(k => k.id === 'home3988')
|
|
expect(kit.price).toBe(3988)
|
|
expect(kit.oils).toHaveLength(23)
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|