import { describe, it, expect } from 'vitest' // --------------------------------------------------------------------------- // Kit definitions // --------------------------------------------------------------------------- describe('Kit definitions', () => { const KITS = { family: ['乳香', '茶树', '薰衣草', '柠檬', '椒样薄荷', '保卫', '牛至', '乐活', '顺畅呼吸', '舒缓'], home3988: ['乳香', '野橘', '柠檬', '薰衣草', '椒样薄荷', '冬青', '茶树', '生姜', '柠檬草', '西班牙牛至', '西洋蓍草石榴籽', '乐活', '保卫', '新瑞活力', '舒缓', '安定情绪', '安宁神气', '顺畅呼吸', '柑橘清新', '芳香调理', '元气焕能'], aroma: ['薰衣草', '舒缓', '安定情绪', '芳香调理', '野橘', '椒样薄荷', '保卫', '茶树'], } it('family kit has 10 oils', () => { expect(KITS.family).toHaveLength(10) }) it('home3988 kit has 21 oils', () => { expect(KITS.home3988).toHaveLength(21) }) it('aroma kit has 8 oils', () => { expect(KITS.aroma).toHaveLength(8) expect(KITS.aroma).toContain('薰衣草') expect(KITS.aroma).toContain('芳香调理') expect(KITS.aroma).toContain('茶树') }) }) // --------------------------------------------------------------------------- // Recipe matching (exclude coconut oil) // --------------------------------------------------------------------------- describe('Recipe matching', () => { function matchRecipe(recipe, ownedSet) { const needed = recipe.ingredients.filter(i => i.oil !== '椰子油').map(i => i.oil) if (needed.length === 0) return false const coverage = needed.filter(o => ownedSet.has(o)).length return coverage >= 1 } it('matches recipe when user has at least one oil', () => { const recipe = { ingredients: [{ oil: '乳香', drops: 3 }, { oil: '茶树', drops: 2 }, { oil: '椰子油', drops: 100 }] } expect(matchRecipe(recipe, new Set(['乳香']))).toBe(true) }) it('does not match when user has none of the oils', () => { const recipe = { ingredients: [{ oil: '乳香', drops: 3 }, { oil: '茶树', drops: 2 }] } expect(matchRecipe(recipe, new Set(['薰衣草']))).toBe(false) }) it('excludes coconut oil from matching', () => { const recipe = { ingredients: [{ oil: '椰子油', drops: 100 }] } expect(matchRecipe(recipe, new Set(['椰子油']))).toBe(false) }) it('matches when user has all oils', () => { const recipe = { ingredients: [{ oil: '乳香', drops: 3 }, { oil: '茶树', drops: 2 }] } expect(matchRecipe(recipe, new Set(['乳香', '茶树']))).toBe(true) }) }) // --------------------------------------------------------------------------- // Consumption analysis // --------------------------------------------------------------------------- describe('Consumption analysis', () => { function calcConsumption(ingredients, oilsMeta) { return ingredients .filter(i => i.oil && i.oil !== '椰子油' && i.drops > 0) .map(i => { const bottleDrops = oilsMeta[i.oil]?.dropCount || 0 const sessions = bottleDrops > 0 ? Math.floor(bottleDrops / i.drops) : 0 return { oil: i.oil, drops: i.drops, bottleDrops, sessions } }) } function findLimit(data) { const valid = data.filter(c => c.sessions > 0) if (!valid.length) return { oil: '', sessions: 0, allSame: true } const min = Math.min(...valid.map(c => c.sessions)) const allSame = valid.every(c => c.sessions === valid[0].sessions) const limiting = valid.find(c => c.sessions === min) return { oil: limiting.oil, sessions: min, allSame } } const meta = { '芳香调理': { dropCount: 280 }, '薰衣草': { dropCount: 280 }, '茶树': { dropCount: 280 }, } it('calculates sessions correctly', () => { const data = calcConsumption([{ oil: '芳香调理', drops: 10 }], meta) expect(data[0].sessions).toBe(28) }) it('finds limiting oil', () => { const data = calcConsumption([ { oil: '芳香调理', drops: 10 }, { oil: '薰衣草', drops: 20 }, ], meta) const limit = findLimit(data) expect(limit.oil).toBe('薰衣草') expect(limit.sessions).toBe(14) expect(limit.allSame).toBe(false) }) it('detects all same sessions', () => { const data = calcConsumption([ { oil: '芳香调理', drops: 10 }, { oil: '茶树', drops: 10 }, ], meta) const limit = findLimit(data) expect(limit.allSame).toBe(true) expect(limit.sessions).toBe(28) }) it('excludes coconut oil', () => { const data = calcConsumption([ { oil: '芳香调理', drops: 10 }, { oil: '椰子油', drops: 200 }, ], meta) expect(data).toHaveLength(1) expect(data[0].oil).toBe('芳香调理') }) }) // --------------------------------------------------------------------------- // Project pricing persistence // --------------------------------------------------------------------------- describe('Project pricing JSON', () => { it('serializes extra fields to JSON', () => { const extra = { selling_price: 299, packaging_cost: 5, labor_cost: 30, other_cost: 10, quantity: 1 } const json = JSON.stringify(extra) const parsed = JSON.parse(json) expect(parsed.selling_price).toBe(299) expect(parsed.quantity).toBe(1) }) it('merges extra fields back into project', () => { const project = { id: 1, name: 'test', ingredients: [], pricing: '{"selling_price":299,"quantity":1}' } const extra = JSON.parse(project.pricing) const merged = { ...project, ...extra } expect(merged.selling_price).toBe(299) expect(merged.quantity).toBe(1) }) })