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:
@@ -106,12 +106,14 @@
|
||||
isLoadingSuggestions = false;
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
isLoadingSuggestions = true;
|
||||
fetch(`/planner?planId=${weekPlan.id}&date=${activePickerDate}`)
|
||||
fetch(`/planner?planId=${weekPlan.id}&date=${activePickerDate}`, { signal: controller.signal })
|
||||
.then((r) => r.json())
|
||||
.then((d) => { suggestions = d.suggestions ?? []; })
|
||||
.catch(() => { suggestions = []; })
|
||||
.catch((e) => { if (e.name !== 'AbortError') suggestions = []; })
|
||||
.finally(() => { isLoadingSuggestions = false; });
|
||||
return () => controller.abort();
|
||||
});
|
||||
|
||||
function handleSelectDay(day: string) {
|
||||
|
||||
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