147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { render, screen, waitFor, within } from '@testing-library/svelte';
|
|
import userEvent from '@testing-library/user-event';
|
|
import Page from './+page.svelte';
|
|
|
|
vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidateAll: vi.fn() }));
|
|
vi.mock('$app/forms', () => ({
|
|
enhance: () => () => ({ destroy: () => {} })
|
|
}));
|
|
|
|
const PLAN_ID = 'plan-00000000-0000-0000-0000-000000000001';
|
|
// Use a past week so "today" is never in this range — selectedDay defaults to weekStart (Monday)
|
|
const DATE = '2025-01-06'; // Monday, January 6 2025
|
|
|
|
const mockData = {
|
|
weekPlan: { id: PLAN_ID, weekStart: DATE, status: 'draft', slots: [] as any[] },
|
|
varietyScore: null,
|
|
weekStart: DATE,
|
|
recipes: [{ id: 'r1', name: 'Beef Bourguignon', effort: 'hard', cookTimeMin: 150 }],
|
|
benutzer: { rolle: 'planer' }
|
|
};
|
|
|
|
const mockDataWithSlot = {
|
|
...mockData,
|
|
weekPlan: {
|
|
id: PLAN_ID,
|
|
weekStart: DATE,
|
|
status: 'draft',
|
|
slots: [{ id: 'slot-1', slotDate: DATE, recipe: { id: 'r1', name: 'Beef Bourguignon', effort: 'hard', cookTimeMin: 150 } }]
|
|
}
|
|
};
|
|
|
|
const mockSuggestions = [
|
|
{
|
|
recipe: { id: 's1', name: 'Lachsfilet', effort: 'easy', cookTimeMin: 20 },
|
|
scoreDelta: 1.5,
|
|
hasConflict: false
|
|
}
|
|
];
|
|
|
|
describe('+page.svelte — $effect suggestion fetch', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('calls fetch when picker opens with correct planId and date', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValueOnce({
|
|
json: () => Promise.resolve({ suggestions: mockSuggestions })
|
|
})
|
|
);
|
|
|
|
render(Page, { props: { data: mockData } });
|
|
|
|
await userEvent.click(screen.getAllByRole('button', { name: /Gericht/i })[0]);
|
|
|
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
expect((fetch as any).mock.calls[0][0]).toContain(`planId=${PLAN_ID}`);
|
|
expect((fetch as any).mock.calls[0][0]).toContain(`date=${DATE}`);
|
|
});
|
|
|
|
it('shows suggestions in RecipePicker after fetch resolves', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValueOnce({
|
|
json: () => Promise.resolve({ suggestions: mockSuggestions })
|
|
})
|
|
);
|
|
|
|
render(Page, { props: { data: mockData } });
|
|
|
|
await userEvent.click(screen.getAllByRole('button', { name: /Gericht/i })[0]);
|
|
|
|
expect(await screen.findByText('Lachsfilet')).toBeTruthy();
|
|
});
|
|
|
|
it('passes AbortSignal to fetch so inflight requests can be cancelled', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValueOnce({
|
|
json: () => Promise.resolve({ suggestions: [] })
|
|
})
|
|
);
|
|
|
|
render(Page, { props: { data: mockData } });
|
|
|
|
await userEvent.click(screen.getAllByRole('button', { name: /Gericht/i })[0]);
|
|
|
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
const fetchOptions = (fetch as any).mock.calls[0][1];
|
|
expect(fetchOptions?.signal).toBeInstanceOf(AbortSignal);
|
|
});
|
|
});
|
|
|
|
describe('+page.svelte — swap sheet suggestion fetch', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('opening mobile swap sheet triggers fetch with planId and date', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve({ suggestions: [] }) }));
|
|
|
|
render(Page, { props: { data: mockDataWithSlot } });
|
|
|
|
// Open action sheet, then swap sheet
|
|
await userEvent.click(screen.getByTestId('day-meal-card'));
|
|
await userEvent.click(await screen.findByRole('button', { name: /Gericht tauschen/i }));
|
|
|
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
expect((fetch as any).mock.calls[0][0]).toContain(`planId=${PLAN_ID}`);
|
|
expect((fetch as any).mock.calls[0][0]).toContain(`date=${DATE}`);
|
|
});
|
|
});
|
|
|
|
describe('+page.svelte — remove meal', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('clicking Entfernen in MealActionSheet shows undo bar with recipe name', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve({ suggestions: [] }) }));
|
|
|
|
render(Page, { props: { data: mockDataWithSlot } });
|
|
|
|
await userEvent.click(screen.getByTestId('day-meal-card'));
|
|
await userEvent.click(await screen.findByRole('button', { name: /Entfernen/i }));
|
|
|
|
const undoBar = screen.getByTestId('undo-bar');
|
|
expect(undoBar).toBeTruthy();
|
|
expect(within(undoBar).getByText(/Beef Bourguignon/)).toBeTruthy();
|
|
});
|
|
|
|
it('clicking Rückgängig after remove hides the undo bar', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve({ suggestions: [] }) }));
|
|
|
|
render(Page, { props: { data: mockDataWithSlot } });
|
|
|
|
await userEvent.click(screen.getByTestId('day-meal-card'));
|
|
await userEvent.click(await screen.findByRole('button', { name: /Entfernen/i }));
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /Rückgängig/i }));
|
|
|
|
expect(screen.queryByTestId('undo-bar')).toBeNull();
|
|
});
|
|
});
|