feat(planner): add AbortController to suggestion fetch $effect
Cancels the inflight request when activePickerDate changes or picker closes, preventing stale responses from overwriting suggestions. Adds page.test.ts covering fetch trigger, suggestion rendering, and AbortSignal presence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
frontend/src/routes/(app)/planner/page.test.ts
Normal file
84
frontend/src/routes/(app)/planner/page.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, screen, waitFor } 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: [] },
|
||||
varietyScore: null,
|
||||
weekStart: DATE,
|
||||
recipes: [{ id: 'r1', name: 'Beef Bourguignon', effort: 'hard', cookTimeMin: 150 }],
|
||||
benutzer: { rolle: 'planer' }
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user