Files
oil-formula-calculator/frontend/src/__tests__/oilCalculations.test.js
Hera Zhao 2491479c2c Add comprehensive test suite: 105 unit + 167 E2E tests
- Vitest unit tests: smart paste parsing (37), cost calculations (21),
  oil translation (16), dialog system (12), with production data fixtures
- Cypress E2E tests: API CRUD (27), auth flow (8), recipe detail (10),
  search (12), oil reference (4), favorites (6), inventory (6),
  recipe management (10), diary (11), bug tracker (8), user management (13),
  cost parity (6), data integrity (8), responsive (9), performance (6),
  navigation (8), admin flow (5)
- Test coverage doc with prioritized gap analysis
- Found backend bug: POST /api/bug-reports/{id}/comment deletes the bug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:47:47 +00:00

193 lines
6.3 KiB
JavaScript

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')
})
})