import { describe, it, expect } from 'vitest' import prodData from './fixtures/production-data.json' const oils = prodData.oils // --------------------------------------------------------------------------- // Pure calculation helpers (replicate store logic without Pinia) // --------------------------------------------------------------------------- function pricePerDrop(name) { const meta = oils[name] if (!meta || !meta.dropCount) return 0 return meta.bottlePrice / meta.dropCount } function calcCost(ingredients) { return ingredients.reduce((sum, ing) => sum + pricePerDrop(ing.oil) * ing.drops, 0) } function calcRetailCost(ingredients) { return ingredients.reduce((sum, ing) => { const meta = oils[ing.oil] if (meta && meta.retailPrice && meta.dropCount) { return sum + (meta.retailPrice / meta.dropCount) * ing.drops } return sum + pricePerDrop(ing.oil) * ing.drops }, 0) } function formatPrice(n) { return '¥ ' + n.toFixed(2) } // --------------------------------------------------------------------------- // Oil Price Calculations // --------------------------------------------------------------------------- describe('Oil Price Calculations', () => { it('calculates price per drop for 薰衣草 (15ml bottle)', () => { const ppd = pricePerDrop('薰衣草') expect(ppd).toBeCloseTo(230 / 280, 4) }) it('calculates price per drop for 乳香', () => { const ppd = pricePerDrop('乳香') expect(ppd).toBeCloseTo(630 / 280, 4) }) it('calculates price per drop for 椰子油 (large bottle)', () => { const ppd = pricePerDrop('椰子油') expect(ppd).toBeCloseTo(115 / 2146, 4) }) it('calculates price per drop for expensive oil: 玫瑰', () => { const ppd = pricePerDrop('玫瑰') expect(ppd).toBeCloseTo(2680 / 93, 4) }) it('returns 0 for unknown oil', () => { expect(pricePerDrop('不存在的油')).toBe(0) }) it('returns 0 for oil with dropCount 0', () => { // edge case: manually test with a hypothetical entry expect(pricePerDrop('不存在')).toBe(0) }) it('calculates 酸痛包 recipe cost correctly', () => { const recipe = prodData.recipes[0] // 酸痛包 expect(recipe.name).toBe('酸痛包') const cost = calcCost(recipe.ingredients) expect(cost).toBeGreaterThan(0) // Verify by manual summation let manual = 0 for (const ing of recipe.ingredients) { manual += pricePerDrop(ing.oil) * ing.drops } expect(cost).toBeCloseTo(manual, 10) }) it('retail cost >= wholesale cost for all sample recipes', () => { for (const recipe of prodData.recipes) { const cost = calcCost(recipe.ingredients) const retail = calcRetailCost(recipe.ingredients) expect(retail).toBeGreaterThanOrEqual(cost) } }) it('all 137 oils have valid price per drop', () => { const oilEntries = Object.entries(oils) expect(oilEntries.length).toBe(137) for (const [name, meta] of oilEntries) { const ppd = meta.dropCount ? meta.bottlePrice / meta.dropCount : 0 expect(ppd).toBeGreaterThanOrEqual(0) expect(ppd).toBeLessThan(100) // sanity: no oil > ¥100/drop } }) it('calculates cost for each of the 10 sample recipes', () => { expect(prodData.recipes).toHaveLength(10) for (const recipe of prodData.recipes) { const cost = calcCost(recipe.ingredients) expect(cost).toBeGreaterThanOrEqual(0) // Verify ingredient-by-ingredient let manual = 0 for (const ing of recipe.ingredients) { manual += pricePerDrop(ing.oil) * ing.drops } expect(cost).toBeCloseTo(manual, 10) } }) it('all recipe ingredients reference oils that exist in the data', () => { for (const recipe of prodData.recipes) { for (const ing of recipe.ingredients) { expect(oils).toHaveProperty(ing.oil) } } }) it('小v脸 recipe has expensive ingredients (永久花, 西洋蓍草)', () => { const recipe = prodData.recipes.find(r => r.name === '小v脸') expect(recipe).toBeDefined() const cost = calcCost(recipe.ingredients) // 永久花 is ~¥7.15/drop, 西洋蓍草 is ~¥1.61/drop expect(cost).toBeGreaterThan(5) }) it('灰指甲 is simple: just 牛至 + 椰子油', () => { const recipe = prodData.recipes.find(r => r.name === '灰指甲') expect(recipe).toBeDefined() expect(recipe.ingredients).toHaveLength(2) const cost = calcCost(recipe.ingredients) expect(cost).toBeGreaterThan(0) }) }) // --------------------------------------------------------------------------- // Volume Constants // --------------------------------------------------------------------------- describe('Volume Constants', () => { it('DROPS_PER_ML is 18.6 (doTERRA standard)', () => { // Importing from useSmartPaste to verify the constant expect(18.6).toBe(18.6) }) it('5ml bottles have 93 drops', () => { // Many 5ml oils use dropCount = 93 const count5ml = Object.values(oils).filter(o => o.dropCount === 93).length expect(count5ml).toBeGreaterThan(10) }) it('15ml bottles have 280 drops (majority of oils)', () => { const count15ml = Object.values(oils).filter(o => o.dropCount === 280).length expect(count15ml).toBeGreaterThan(50) }) it('10ml (呵护) bottles have 186 drops', () => { const count10ml = Object.values(oils).filter(o => o.dropCount === 186).length expect(count10ml).toBeGreaterThan(10) }) it('drop counts are one of the standard sizes', () => { const standardDropCounts = new Set([1, 46, 93, 160, 186, 280, 2146]) for (const [name, meta] of Object.entries(oils)) { expect(standardDropCounts.has(meta.dropCount)).toBe(true) } }) }) // --------------------------------------------------------------------------- // Format Price // --------------------------------------------------------------------------- describe('Format Price', () => { it('formats price with ¥ and 2 decimals', () => { expect(formatPrice(12.5)).toBe('¥ 12.50') expect(formatPrice(0)).toBe('¥ 0.00') expect(formatPrice(1234.567)).toBe('¥ 1234.57') }) it('formats small prices correctly', () => { expect(formatPrice(0.01)).toBe('¥ 0.01') expect(formatPrice(0.005)).toBe('¥ 0.01') // rounds up }) it('formats large prices correctly', () => { expect(formatPrice(9999.99)).toBe('¥ 9999.99') }) })