import { describe, it, expect } from 'vitest' import { EDITOR_ONLY_TAGS } from '../stores/recipes' // --------------------------------------------------------------------------- // Tag sorting // --------------------------------------------------------------------------- describe('Tag sorting', () => { it('sorts tags alphabetically with localeCompare zh', () => { const tags = ['香水', '呼吸', '消化', '美容'] const sorted = [...tags].sort((a, b) => a.localeCompare(b, 'zh')) expect(sorted[0]).toBe('呼吸') // All sorted for (let i = 1; i < sorted.length; i++) { expect(sorted[i - 1].localeCompare(sorted[i], 'zh')).toBeLessThanOrEqual(0) } }) it('EDITOR_ONLY_TAGS filters correctly', () => { const allTags = ['呼吸', '已审核', '消化', '香水'] const visible = allTags.filter(t => !EDITOR_ONLY_TAGS.includes(t)) expect(visible).not.toContain('已审核') expect(visible).toContain('呼吸') expect(visible).toHaveLength(3) }) }) // --------------------------------------------------------------------------- // Recipe save data format // --------------------------------------------------------------------------- describe('Recipe data format', () => { it('oil_name format overwrites oil format when spread', () => { // This test documents the bug that was fixed const localRecipe = { ingredients: [{ oil: '薰衣草', drops: 3 }], } const payload = { ingredients: [{ oil_name: '薰衣草', drops: 3 }], } const merged = { ...localRecipe, ...payload } // After merge, ingredients have oil_name not oil — this was the bug expect(merged.ingredients[0]).toHaveProperty('oil_name') expect(merged.ingredients[0]).not.toHaveProperty('oil') }) it('loadRecipes mapping converts oil_name to oil', () => { // Simulate what loadRecipes does const apiData = [ { id: 1, name: 'test', ingredients: [{ oil_name: '薰衣草', drops: 3 }], tags: [] } ] const mapped = apiData.map(r => ({ ...r, ingredients: r.ingredients.map(ing => ({ oil: ing.oil_name ?? ing.oil, drops: ing.drops, })) })) expect(mapped[0].ingredients[0].oil).toBe('薰衣草') expect(mapped[0].ingredients[0]).not.toHaveProperty('oil_name') }) }) // --------------------------------------------------------------------------- // Volume detection from ingredients // --------------------------------------------------------------------------- describe('Volume detection', () => { const DROPS_PER_ML = 18.6 function guessVolume(eoDrops, cocoDrops) { const totalDrops = eoDrops + cocoDrops const ml = totalDrops / DROPS_PER_ML if (ml <= 2) return 'single' if (Math.abs(ml - 5) < 1.5) return '5' if (Math.abs(ml - 10) < 2.5) return '10' if (Math.abs(ml - 15) < 2.5) return '15' if (Math.abs(ml - 20) < 3) return '20' if (Math.abs(ml - 30) < 6) return '30' return 'custom' } it('detects single use (small amounts)', () => { expect(guessVolume(5, 10)).toBe('single') }) it('detects 5ml', () => { expect(guessVolume(15, Math.round(5 * DROPS_PER_ML) - 15)).toBe('5') }) it('detects 10ml', () => { expect(guessVolume(20, Math.round(10 * DROPS_PER_ML) - 20)).toBe('10') }) it('detects 30ml', () => { expect(guessVolume(50, Math.round(30 * DROPS_PER_ML) - 50)).toBe('30') }) it('no coconut returns no volume', () => { // When cocoDrops is 0, function still returns based on total // But in real code, no coconut → formVolume = '' expect(guessVolume(10, 0)).toBe('single') }) it('detects custom for large volumes', () => { expect(guessVolume(100, 1000)).toBe('custom') }) }) // --------------------------------------------------------------------------- // Dilution ratio calculation // --------------------------------------------------------------------------- describe('Dilution ratio', () => { it('calculates ratio correctly', () => { expect(Math.round(60 / 10)).toBe(6) // 1:6 expect(Math.round(30 / 10)).toBe(3) // 1:3 expect(Math.round(100 / 10)).toBe(10) // 1:10 }) it('snaps to nearest option', () => { const options = [3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20] const snap = (ratio) => options.reduce((a, b) => Math.abs(b - ratio) < Math.abs(a - ratio) ? b : a) expect(snap(6)).toBe(6) expect([10, 12]).toContain(snap(11)) // equidistant expect(snap(13)).toBe(12) expect([12, 15]).toContain(snap(14)) expect(snap(18)).toBe(20) }) })