import { describe, it, expect, beforeEach } from 'vitest'; import type Database from 'better-sqlite3'; import { openInMemoryForTest } from '../../src/lib/server/db'; import { createProfile } from '../../src/lib/server/profiles/repository'; import { insertRecipe } from '../../src/lib/server/recipes/repository'; import { addToWishlist, removeFromWishlist, listWishlist, isOnWishlist, likeWish, unlikeWish } from '../../src/lib/server/wishlist/repository'; import type { Recipe } from '../../src/lib/types'; const recipe = (title: string, id?: null): Recipe => ({ id: id ?? null, title, description: null, source_url: null, source_domain: null, image_path: null, servings_default: 4, servings_unit: null, prep_time_min: null, cook_time_min: null, total_time_min: null, cuisine: null, category: null, ingredients: [], steps: [], tags: [] }); let db: Database.Database; beforeEach(() => { db = openInMemoryForTest(); }); describe('wishlist add/remove', () => { it('adds and lists', () => { const r1 = insertRecipe(db, recipe('Carbonara')); const p = createProfile(db, 'Hendrik'); addToWishlist(db, r1, p.id); expect(isOnWishlist(db, r1)).toBe(true); const list = listWishlist(db, p.id); expect(list.length).toBe(1); expect(list[0].title).toBe('Carbonara'); expect(list[0].added_by_name).toBe('Hendrik'); }); it('is idempotent on double-add', () => { const r1 = insertRecipe(db, recipe('Pizza')); const p = createProfile(db, 'A'); addToWishlist(db, r1, p.id); addToWishlist(db, r1, p.id); expect(listWishlist(db, p.id).length).toBe(1); }); it('removes', () => { const r1 = insertRecipe(db, recipe('X')); addToWishlist(db, r1, null); removeFromWishlist(db, r1); expect(listWishlist(db, null).length).toBe(0); }); it('cascades with recipe delete', () => { const r1 = insertRecipe(db, recipe('X')); addToWishlist(db, r1, null); db.prepare('DELETE FROM recipe WHERE id = ?').run(r1); expect(listWishlist(db, null).length).toBe(0); }); }); describe('wishlist likes + sort', () => { it('counts likes per entry and shows liked_by_me for active profile', () => { const r1 = insertRecipe(db, recipe('R1')); const r2 = insertRecipe(db, recipe('R2')); const a = createProfile(db, 'A'); const b = createProfile(db, 'B'); const c = createProfile(db, 'C'); addToWishlist(db, r1, a.id); addToWishlist(db, r2, a.id); likeWish(db, r1, a.id); likeWish(db, r1, b.id); likeWish(db, r1, c.id); likeWish(db, r2, a.id); const listA = listWishlist(db, a.id, 'popular'); expect(listA[0].title).toBe('R1'); expect(listA[0].like_count).toBe(3); expect(listA[0].liked_by_me).toBe(1); expect(listA[1].title).toBe('R2'); expect(listA[1].like_count).toBe(1); const listB = listWishlist(db, b.id); expect(listB.find((e) => e.recipe_id === r1)!.liked_by_me).toBe(1); expect(listB.find((e) => e.recipe_id === r2)!.liked_by_me).toBe(0); }); it('unlike is idempotent and decrements count', () => { const r = insertRecipe(db, recipe('R')); const a = createProfile(db, 'A'); addToWishlist(db, r, a.id); likeWish(db, r, a.id); unlikeWish(db, r, a.id); unlikeWish(db, r, a.id); const [entry] = listWishlist(db, a.id); expect(entry.like_count).toBe(0); expect(entry.liked_by_me).toBe(0); }); it('sort=newest orders by added_at desc, oldest asc', () => { const r1 = insertRecipe(db, recipe('First')); // Force different timestamps via raw insert with explicit added_at db.prepare("INSERT INTO wishlist(recipe_id, added_at) VALUES (?, '2026-01-01 10:00:00')").run(r1); const r2 = insertRecipe(db, recipe('Second')); db.prepare("INSERT INTO wishlist(recipe_id, added_at) VALUES (?, '2026-01-02 10:00:00')").run(r2); expect(listWishlist(db, null, 'newest').map((e) => e.title)).toEqual(['Second', 'First']); expect(listWishlist(db, null, 'oldest').map((e) => e.title)).toEqual(['First', 'Second']); }); it('handles anonymous (no active profile) — liked_by_me always 0', () => { const r = insertRecipe(db, recipe('R')); addToWishlist(db, r, null); likeWish(db, r, createProfile(db, 'A').id); const [entry] = listWishlist(db, null); expect(entry.like_count).toBe(1); expect(entry.liked_by_me).toBe(0); }); });