All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s
Wenn ein Rezept heute gekocht wurde, ist der Wunsch eingelöst — raus damit aus der Wunschliste aller Profile. Server tut das beim POST in einem Rutsch (removeFromWishlistForAll) und meldet removed_from_wishlist in der Response zurück. Der Client räumt daraufhin den lokalen wishlistProfileIds-State und refresht den Badge-Zähler, damit der Wunschliste-Button und das Header-Badge sofort passen — kein Reload nötig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
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,
|
|
removeFromWishlistForAll,
|
|
listWishlist,
|
|
listWishlistProfileIds,
|
|
isOnMyWishlist,
|
|
countWishlistRecipes
|
|
} 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('per-user wishlist', () => {
|
|
it('adds and lists for a single profile', () => {
|
|
const r1 = insertRecipe(db, recipe('Carbonara'));
|
|
const p = createProfile(db, 'Hendrik');
|
|
addToWishlist(db, r1, p.id);
|
|
expect(isOnMyWishlist(db, r1, p.id)).toBe(true);
|
|
|
|
const list = listWishlist(db, p.id);
|
|
expect(list.length).toBe(1);
|
|
expect(list[0].title).toBe('Carbonara');
|
|
expect(list[0].wanted_by_count).toBe(1);
|
|
expect(list[0].wanted_by_names).toBe('Hendrik');
|
|
expect(list[0].on_my_wishlist).toBe(1);
|
|
});
|
|
|
|
it('aggregates multiple users per recipe', () => {
|
|
const r1 = insertRecipe(db, recipe('Pizza'));
|
|
const a = createProfile(db, 'Alice');
|
|
const b = createProfile(db, 'Bob');
|
|
const c = createProfile(db, 'Cara');
|
|
addToWishlist(db, r1, a.id);
|
|
addToWishlist(db, r1, b.id);
|
|
addToWishlist(db, r1, c.id);
|
|
|
|
const listFromA = listWishlist(db, a.id);
|
|
expect(listFromA.length).toBe(1);
|
|
expect(listFromA[0].wanted_by_count).toBe(3);
|
|
expect(listFromA[0].on_my_wishlist).toBe(1);
|
|
|
|
const ids = listWishlistProfileIds(db, r1);
|
|
expect(ids.sort()).toEqual([a.id, b.id, c.id].sort());
|
|
});
|
|
|
|
it('is idempotent on double-add for same profile', () => {
|
|
const r1 = insertRecipe(db, recipe('Pizza'));
|
|
const p = createProfile(db, 'A');
|
|
addToWishlist(db, r1, p.id);
|
|
addToWishlist(db, r1, p.id);
|
|
const list = listWishlist(db, p.id);
|
|
expect(list[0].wanted_by_count).toBe(1);
|
|
});
|
|
|
|
it('removes only my entry, keeps others', () => {
|
|
const r1 = insertRecipe(db, recipe('Salad'));
|
|
const a = createProfile(db, 'A');
|
|
const b = createProfile(db, 'B');
|
|
addToWishlist(db, r1, a.id);
|
|
addToWishlist(db, r1, b.id);
|
|
removeFromWishlist(db, r1, a.id);
|
|
expect(isOnMyWishlist(db, r1, a.id)).toBe(false);
|
|
expect(isOnMyWishlist(db, r1, b.id)).toBe(true);
|
|
expect(listWishlist(db, b.id)[0].wanted_by_count).toBe(1);
|
|
});
|
|
|
|
it('on_my_wishlist is 0 for profiles that did not wish', () => {
|
|
const r1 = insertRecipe(db, recipe('Curry'));
|
|
const a = createProfile(db, 'A');
|
|
const b = createProfile(db, 'B');
|
|
addToWishlist(db, r1, a.id);
|
|
|
|
const listFromB = listWishlist(db, b.id);
|
|
expect(listFromB[0].on_my_wishlist).toBe(0);
|
|
expect(listFromB[0].wanted_by_count).toBe(1);
|
|
});
|
|
|
|
it('cascades when recipe is deleted', () => {
|
|
const r1 = insertRecipe(db, recipe('X'));
|
|
const a = createProfile(db, 'A');
|
|
addToWishlist(db, r1, a.id);
|
|
db.prepare('DELETE FROM recipe WHERE id = ?').run(r1);
|
|
expect(listWishlist(db, a.id).length).toBe(0);
|
|
});
|
|
|
|
it('cascades when profile is deleted', () => {
|
|
const r1 = insertRecipe(db, recipe('X'));
|
|
const a = createProfile(db, 'A');
|
|
addToWishlist(db, r1, a.id);
|
|
db.prepare('DELETE FROM profile WHERE id = ?').run(a.id);
|
|
expect(listWishlist(db, null).length).toBe(0);
|
|
});
|
|
|
|
it('countWishlistRecipes counts distinct recipes (not rows)', () => {
|
|
const r1 = insertRecipe(db, recipe('R1'));
|
|
const r2 = insertRecipe(db, recipe('R2'));
|
|
const a = createProfile(db, 'A');
|
|
const b = createProfile(db, 'B');
|
|
addToWishlist(db, r1, a.id);
|
|
addToWishlist(db, r1, b.id); // same recipe, different user
|
|
addToWishlist(db, r2, a.id);
|
|
expect(countWishlistRecipes(db)).toBe(2);
|
|
});
|
|
|
|
it('removeFromWishlistForAll drops every profile', () => {
|
|
const r1 = insertRecipe(db, recipe('R1'));
|
|
const a = createProfile(db, 'A');
|
|
const b = createProfile(db, 'B');
|
|
addToWishlist(db, r1, a.id);
|
|
addToWishlist(db, r1, b.id);
|
|
removeFromWishlistForAll(db, r1);
|
|
expect(listWishlistProfileIds(db, r1)).toEqual([]);
|
|
});
|
|
});
|