import { describe, it, expect } from 'vitest' import { recipeNameEn, oilEn } from '../composables/useOilTranslation' import { matchesPinyinInitials, getPinyinInitials } from '../composables/usePinyinMatch' // --------------------------------------------------------------------------- // EDITOR_ONLY_TAGS includes '已下架' // --------------------------------------------------------------------------- describe('EDITOR_ONLY_TAGS', () => { it('includes 已审核', async () => { const { EDITOR_ONLY_TAGS } = await import('../stores/recipes') expect(EDITOR_ONLY_TAGS).toContain('已审核') }) it('includes 已下架', async () => { const { EDITOR_ONLY_TAGS } = await import('../stores/recipes') expect(EDITOR_ONLY_TAGS).toContain('已下架') }) it('is an array with at least 2 entries', async () => { const { EDITOR_ONLY_TAGS } = await import('../stores/recipes') expect(Array.isArray(EDITOR_ONLY_TAGS)).toBe(true) expect(EDITOR_ONLY_TAGS.length).toBeGreaterThanOrEqual(2) }) }) // --------------------------------------------------------------------------- // English drop/drops pluralization logic // --------------------------------------------------------------------------- describe('drop/drops pluralization', () => { const pluralize = (n) => (n === 1 ? 'drop' : 'drops') it('singular: 1 drop', () => { expect(pluralize(1)).toBe('drop') }) it('plural: 0 drops', () => { expect(pluralize(0)).toBe('drops') }) it('plural: 2 drops', () => { expect(pluralize(2)).toBe('drops') }) it('plural: 5 drops', () => { expect(pluralize(5)).toBe('drops') }) }) // --------------------------------------------------------------------------- // 已下架 tag filtering logic (pure function extraction) // --------------------------------------------------------------------------- describe('已下架 tag filtering', () => { const recipes = [ { name: 'Active Recipe', tags: ['头疗'] }, { name: 'Delisted Recipe', tags: ['已下架'] }, { name: 'No Tags Recipe', tags: [] }, { name: 'Multi Tag', tags: ['热门', '已下架'] }, { name: 'Null Tags', tags: null }, ] const filterDelisted = (list) => list.filter((r) => !r.tags || !r.tags.includes('已下架')) it('removes recipes with 已下架 tag', () => { const result = filterDelisted(recipes) expect(result.map((r) => r.name)).not.toContain('Delisted Recipe') expect(result.map((r) => r.name)).not.toContain('Multi Tag') }) it('keeps recipes without 已下架 tag', () => { const result = filterDelisted(recipes) expect(result.map((r) => r.name)).toContain('Active Recipe') expect(result.map((r) => r.name)).toContain('No Tags Recipe') }) it('handles null tags gracefully', () => { const result = filterDelisted(recipes) expect(result.map((r) => r.name)).toContain('Null Tags') }) it('returns empty array for all-delisted list', () => { const all = [ { name: 'A', tags: ['已下架'] }, { name: 'B', tags: ['已下架', '其他'] }, ] expect(filterDelisted(all)).toHaveLength(0) }) }) // --------------------------------------------------------------------------- // recipeNameEn — front-end keyword translation // --------------------------------------------------------------------------- describe('recipeNameEn', () => { it('translates 酸痛包 → Pain Relief Blend', () => { expect(recipeNameEn('酸痛包')).toBe('Pain Relief Blend') }) it('translates 助眠配方 → Sleep Aid Blend', () => { expect(recipeNameEn('助眠配方')).toBe('Sleep Aid Blend') }) it('translates 头痛 → Headache', () => { expect(recipeNameEn('头痛')).toBe('Headache') }) it('translates 肩颈按摩 → Neck & Shoulder Massage', () => { expect(recipeNameEn('肩颈按摩')).toBe('Neck & Shoulder Massage') }) it('translates 湿疹舒缓 → Eczema Soothing', () => { expect(recipeNameEn('湿疹舒缓')).toBe('Eczema Soothing') }) it('translates 淋巴排毒 → Lymph Detox', () => { expect(recipeNameEn('淋巴排毒')).toBe('Lymph Detox') }) it('translates 灰指甲 → Nail Fungus', () => { expect(recipeNameEn('灰指甲')).toBe('Nail Fungus') }) it('translates 缓解焦虑 → Relief Anxiety', () => { expect(recipeNameEn('缓解焦虑')).toBe('Relief Anxiety') }) it('returns original name for unknown text', () => { expect(recipeNameEn('XYZXYZ')).toBe('XYZXYZ') }) it('returns empty/null for empty/null input', () => { expect(recipeNameEn('')).toBe('') expect(recipeNameEn(null)).toBeNull() }) it('does not duplicate keywords', () => { // 酸痛 maps to Pain Relief; should not appear twice const result = recipeNameEn('酸痛酸痛') expect(result).toBe('Pain Relief') }) }) // --------------------------------------------------------------------------- // Duplicate oil prevention logic // --------------------------------------------------------------------------- describe('duplicate oil prevention', () => { it('detects duplicate oil in ingredient list', () => { const ings = [ { oil: '薰衣草', drops: 3 }, { oil: '茶树', drops: 2 }, ] const newOil = '薰衣草' const isDup = ings.some(i => i.oil === newOil) expect(isDup).toBe(true) }) it('allows non-duplicate oil', () => { const ings = [ { oil: '薰衣草', drops: 3 }, { oil: '茶树', drops: 2 }, ] const newOil = '乳香' const isDup = ings.some(i => i.oil === newOil) expect(isDup).toBe(false) }) it('allows same oil for the same row (editing current)', () => { const ing = { oil: '薰衣草', drops: 3 } const ings = [ing, { oil: '茶树', drops: 2 }] // When selecting for the same row, exclude self const isDup = ings.some(i => i !== ing && i.oil === '薰衣草') expect(isDup).toBe(false) }) it('handles empty ingredient list (no duplicates)', () => { const ings = [] const isDup = ings.some(i => i.oil === '薰衣草') expect(isDup).toBe(false) }) }) // --------------------------------------------------------------------------- // recipeNameEn — additional edge cases for PR28 // --------------------------------------------------------------------------- describe('recipeNameEn — PR28 additional cases', () => { it('translates 排毒配方 → Detox Blend', () => { expect(recipeNameEn('排毒配方')).toBe('Detox Blend') }) it('translates 呼吸系统护理 → Respiratory System Care', () => { expect(recipeNameEn('呼吸系统护理')).toBe('Respiratory System Care') }) it('translates 儿童助眠 → Children\'s Sleep Aid', () => { expect(recipeNameEn('儿童助眠')).toBe("Children's Sleep Aid") }) it('translates 美容按摩 → Beauty Massage', () => { expect(recipeNameEn('美容按摩')).toBe('Beauty Massage') }) it('handles mixed Chinese and ASCII text', () => { // Unknown Chinese chars are skipped; if ASCII appears, it's kept const result = recipeNameEn('testBlend') // No Chinese keyword matches, falls back to original expect(result).toBe('testBlend') }) it('handles single-keyword name', () => { expect(recipeNameEn('免疫')).toBe('Immunity') }) it('translates compound: 肩颈按摩配方 → Neck & Shoulder Massage Blend', () => { expect(recipeNameEn('肩颈按摩配方')).toBe('Neck & Shoulder Massage Blend') }) }) // --------------------------------------------------------------------------- // oilEn — English oil name translation // --------------------------------------------------------------------------- describe('oilEn', () => { it('translates known oils', () => { expect(oilEn('薰衣草')).toBe('Lavender') expect(oilEn('茶树')).toBe('Tea Tree') expect(oilEn('乳香')).toBe('Frankincense') }) it('handles 复方 suffix removal', () => { expect(oilEn('舒缓复方')).toBe('Past Tense') }) it('handles 复方 suffix addition', () => { // '呼吸' maps via '呼吸复方' → 'Breathe' expect(oilEn('呼吸')).toBe('Breathe') }) it('returns empty string for unknown oil', () => { expect(oilEn('不存在的油')).toBe('') }) }) // --------------------------------------------------------------------------- // Case-insensitive username logic (pure function) // --------------------------------------------------------------------------- describe('case-insensitive username matching', () => { const matchCaseInsensitive = (input, existing) => existing.some(u => u.toLowerCase() === input.toLowerCase()) it('detects duplicate usernames case-insensitively', () => { const existing = ['TestUser', 'Alice', 'Bob'] expect(matchCaseInsensitive('testuser', existing)).toBe(true) expect(matchCaseInsensitive('TESTUSER', existing)).toBe(true) expect(matchCaseInsensitive('TestUser', existing)).toBe(true) }) it('allows unique username', () => { const existing = ['TestUser', 'Alice'] expect(matchCaseInsensitive('Charlie', existing)).toBe(false) }) it('is case-insensitive for mixed-case inputs', () => { const existing = ['alice'] expect(matchCaseInsensitive('Alice', existing)).toBe(true) expect(matchCaseInsensitive('ALICE', existing)).toBe(true) expect(matchCaseInsensitive('aLiCe', existing)).toBe(true) }) }) // --------------------------------------------------------------------------- // One-time username change logic // --------------------------------------------------------------------------- describe('one-time username change guard', () => { it('blocks rename when username_changed is truthy', () => { const user = { username_changed: 1 } expect(!!user.username_changed).toBe(true) }) it('allows rename when username_changed is falsy', () => { const user = { username_changed: 0 } expect(!!user.username_changed).toBe(false) }) it('allows rename when username_changed is undefined', () => { const user = {} expect(!!user.username_changed).toBe(false) }) }) // --------------------------------------------------------------------------- // Pinyin matching — PR29 extended coverage // --------------------------------------------------------------------------- describe('pinyin matching — extended oil names', () => { it('matches mlk → 麦卢卡', () => { expect(matchesPinyinInitials('麦卢卡', 'mlk')).toBe(true) }) it('matches tx → 檀香', () => { expect(matchesPinyinInitials('檀香', 'tx')).toBe(true) }) it('matches xm → 香茅', () => { expect(matchesPinyinInitials('香茅', 'xm')).toBe(true) }) it('matches gbxz → 古巴香脂', () => { expect(matchesPinyinInitials('古巴香脂', 'gbxz')).toBe(true) }) it('matches my → 没药', () => { expect(matchesPinyinInitials('没药', 'my')).toBe(true) }) it('matches xhx → 小茴香', () => { expect(matchesPinyinInitials('小茴香', 'xhx')).toBe(true) }) it('matches jybh → 椒样薄荷', () => { expect(matchesPinyinInitials('椒样薄荷', 'jybh')).toBe(true) }) it('matches xbynz → 西班牙牛至', () => { expect(matchesPinyinInitials('西班牙牛至', 'xbynz')).toBe(true) }) it('matches sc → 顺畅呼吸 prefix', () => { expect(matchesPinyinInitials('顺畅呼吸', 'sc')).toBe(true) }) it('does not match wrong initials', () => { expect(matchesPinyinInitials('麦卢卡', 'abc')).toBe(false) }) it('getPinyinInitials returns correct string', () => { expect(getPinyinInitials('麦卢卡')).toBe('mlk') expect(getPinyinInitials('檀香')).toBe('tx') expect(getPinyinInitials('没药')).toBe('my') }) }) // --------------------------------------------------------------------------- // Viewer tag visibility — PR29 // --------------------------------------------------------------------------- describe('viewer tag visibility logic', () => { const EDITOR_ONLY_TAGS_VAL = ['已审核', '已下架'] it('editor sees all tags', () => { const allTags = ['美容', '儿童', '已审核', '已下架'] const canEdit = true const visible = canEdit ? allTags : [] expect(visible).toEqual(allTags) }) it('viewer sees no public tags', () => { const canEdit = false const myDiary = [ { tags: ['我的标签'] }, { tags: ['我的标签', '另一个'] }, ] // Viewer: collect tags from own diary only const myTags = new Set() for (const d of myDiary) { for (const t of (d.tags || [])) myTags.add(t) } const visible = canEdit ? ['美容', '已审核'] : [...myTags] expect(visible).toContain('我的标签') expect(visible).toContain('另一个') expect(visible).not.toContain('美容') expect(visible).not.toContain('已审核') }) it('viewer with no diary tags sees empty', () => { const myDiary = [] const myTags = new Set() for (const d of myDiary) { for (const t of (d.tags || [])) myTags.add(t) } expect([...myTags]).toHaveLength(0) }) })