fix(staples): pass ctx from URL through load function; fix script order in page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { apiClient } from '$lib/server/api';
|
import { apiClient } from '$lib/server/api';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch }) => {
|
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||||
const api = apiClient(fetch);
|
const api = apiClient(fetch);
|
||||||
|
|
||||||
const [categoriesResult, ingredientsResult] = await Promise.all([
|
const [categoriesResult, ingredientsResult] = await Promise.all([
|
||||||
@@ -24,5 +24,5 @@ export const load: PageServerLoad = async ({ fetch }) => {
|
|||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { categories };
|
return { categories, ctx: url.searchParams.get('ctx') };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<svelte:head>
|
|
||||||
<title>Vorräte einrichten — Mealplan</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ProgressSidebar from '$lib/components/ProgressSidebar.svelte';
|
import ProgressSidebar from '$lib/components/ProgressSidebar.svelte';
|
||||||
import StaplesManager from '$lib/onboarding/StaplesManager.svelte';
|
import StaplesManager from '$lib/onboarding/StaplesManager.svelte';
|
||||||
|
|
||||||
type Category = { id: string; name: string; ingredients: { id: string; name: string; isStaple: boolean }[] };
|
type Category = { id: string; name: string; ingredients: { id: string; name: string; isStaple: boolean }[] };
|
||||||
|
|
||||||
let { data, ctx }: { data: { categories: Category[] }; ctx?: string } = $props();
|
let { data }: { data: { categories: Category[]; ctx: string | null } } = $props();
|
||||||
|
|
||||||
const isOnboarding = $derived(ctx === 'onboarding');
|
const isOnboarding = $derived(data.ctx === 'onboarding');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Vorräte einrichten — Mealplan</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
{#if isOnboarding}
|
{#if isOnboarding}
|
||||||
<div class="flex min-h-screen bg-[var(--color-page)]">
|
<div class="flex min-h-screen bg-[var(--color-page)]">
|
||||||
<!-- Desktop sidebar -->
|
<!-- Desktop sidebar -->
|
||||||
|
|||||||
@@ -41,9 +41,23 @@ describe('household staples page — load', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('passes ctx from url searchParams into returned data', async () => {
|
||||||
|
mockApiResponses();
|
||||||
|
const url = new URL('http://localhost/household/staples?ctx=onboarding');
|
||||||
|
const result = await load({ fetch: vi.fn(), url } as any);
|
||||||
|
expect(result.ctx).toBe('onboarding');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns ctx as null when no ctx param is present', async () => {
|
||||||
|
mockApiResponses();
|
||||||
|
const url = new URL('http://localhost/household/staples');
|
||||||
|
const result = await load({ fetch: vi.fn(), url } as any);
|
||||||
|
expect(result.ctx).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('fetches both categories and ingredients in parallel', async () => {
|
it('fetches both categories and ingredients in parallel', async () => {
|
||||||
mockApiResponses();
|
mockApiResponses();
|
||||||
await load({ fetch: vi.fn() } as any);
|
await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any);
|
||||||
|
|
||||||
const calls = mockGet.mock.calls.map((c) => c[0]);
|
const calls = mockGet.mock.calls.map((c) => c[0]);
|
||||||
expect(calls).toContain('/v1/ingredient-categories');
|
expect(calls).toContain('/v1/ingredient-categories');
|
||||||
@@ -52,7 +66,7 @@ describe('household staples page — load', () => {
|
|||||||
|
|
||||||
it('groups ingredients by category id', async () => {
|
it('groups ingredients by category id', async () => {
|
||||||
mockApiResponses();
|
mockApiResponses();
|
||||||
const result = await load({ fetch: vi.fn() } as any);
|
const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any);
|
||||||
|
|
||||||
expect(result.categories).toHaveLength(2);
|
expect(result.categories).toHaveLength(2);
|
||||||
const oele = result.categories.find((c: any) => c.id === 'cat-1');
|
const oele = result.categories.find((c: any) => c.id === 'cat-1');
|
||||||
@@ -62,7 +76,7 @@ describe('household staples page — load', () => {
|
|||||||
|
|
||||||
it('preserves isStaple flag on each ingredient', async () => {
|
it('preserves isStaple flag on each ingredient', async () => {
|
||||||
mockApiResponses();
|
mockApiResponses();
|
||||||
const result = await load({ fetch: vi.fn() } as any);
|
const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any);
|
||||||
|
|
||||||
const oele = result.categories.find((c: any) => c.id === 'cat-1');
|
const oele = result.categories.find((c: any) => c.id === 'cat-1');
|
||||||
expect(oele.ingredients.find((i: any) => i.name === 'Olivenöl').isStaple).toBe(true);
|
expect(oele.ingredients.find((i: any) => i.name === 'Olivenöl').isStaple).toBe(true);
|
||||||
@@ -79,7 +93,7 @@ describe('household staples page — load', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await load({ fetch: vi.fn() } as any);
|
const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any);
|
||||||
const leer = result.categories.find((c: any) => c.id === 'cat-3');
|
const leer = result.categories.find((c: any) => c.id === 'cat-3');
|
||||||
expect(leer).toBeDefined();
|
expect(leer).toBeDefined();
|
||||||
expect(leer.ingredients).toHaveLength(0);
|
expect(leer.ingredients).toHaveLength(0);
|
||||||
@@ -87,7 +101,7 @@ describe('household staples page — load', () => {
|
|||||||
|
|
||||||
it('returns empty categories when API fails', async () => {
|
it('returns empty categories when API fails', async () => {
|
||||||
mockGet.mockResolvedValue({ data: undefined, error: { status: 500 } });
|
mockGet.mockResolvedValue({ data: undefined, error: { status: 500 } });
|
||||||
const result = await load({ fetch: vi.fn() } as any);
|
const result = await load({ fetch: vi.fn(), url: new URL('http://localhost/household/staples') } as any);
|
||||||
expect(result.categories).toEqual([]);
|
expect(result.categories).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||||||
import { render, screen } from '@testing-library/svelte';
|
import { render, screen } from '@testing-library/svelte';
|
||||||
import Page from './+page.svelte';
|
import Page from './+page.svelte';
|
||||||
|
|
||||||
vi.mock('$app/state', () => ({
|
|
||||||
page: { url: { searchParams: { get: vi.fn() } } }
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockCategories = [
|
const mockCategories = [
|
||||||
{
|
{
|
||||||
id: 'cat-1',
|
id: 'cat-1',
|
||||||
@@ -27,34 +23,34 @@ describe('staples page — onboarding context (?ctx=onboarding)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders ProgressSidebar with step 2 active', () => {
|
it('renders ProgressSidebar with step 2 active', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } });
|
||||||
expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step');
|
expect(screen.getByTestId('step-2')).toHaveAttribute('aria-current', 'step');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Continue button linking to /household/invite', () => {
|
it('renders Continue button linking to /household/invite', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } });
|
||||||
const continueLink = screen.getByRole('link', { name: /weiter/i });
|
const continueLink = screen.getByRole('link', { name: /weiter/i });
|
||||||
expect(continueLink).toHaveAttribute('href', '/household/invite');
|
expect(continueLink).toHaveAttribute('href', '/household/invite');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Skip button linking to /planner', () => {
|
it('renders Skip button linking to /planner', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } });
|
||||||
const skipLink = screen.getByRole('link', { name: /überspringen/i });
|
const skipLink = screen.getByRole('link', { name: /überspringen/i });
|
||||||
expect(skipLink).toHaveAttribute('href', '/planner');
|
expect(skipLink).toHaveAttribute('href', '/planner');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the StaplesManager with categories', () => {
|
it('renders the StaplesManager with categories', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } });
|
||||||
expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
|
expect(screen.getByText('Öle & Fette')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the page title', () => {
|
it('sets the page title', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } });
|
||||||
expect(document.title).toBe('Vorräte einrichten — Mealplan');
|
expect(document.title).toBe('Vorräte einrichten — Mealplan');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders mobile step indicator Schritt 2 von 3', () => {
|
it('renders mobile step indicator Schritt 2 von 3', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories }, ctx: 'onboarding' } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: 'onboarding' } } });
|
||||||
expect(screen.getByText(/schritt 2 von 3/i)).toBeInTheDocument();
|
expect(screen.getByText(/schritt 2 von 3/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -69,18 +65,18 @@ describe('staples page — settings context (no ctx)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not render ProgressSidebar', () => {
|
it('does not render ProgressSidebar', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories } } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: null } } });
|
||||||
expect(screen.queryByTestId('step-1')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('step-1')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render Continue or Skip buttons', () => {
|
it('does not render Continue or Skip buttons', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories } } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: null } } });
|
||||||
expect(screen.queryByRole('link', { name: /weiter/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('link', { name: /weiter/i })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole('link', { name: /überspringen/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole('link', { name: /überspringen/i })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a settings heading', () => {
|
it('renders a settings heading', () => {
|
||||||
render(Page, { props: { data: { categories: mockCategories } } });
|
render(Page, { props: { data: { categories: mockCategories, ctx: null } } });
|
||||||
expect(screen.getByRole('heading', { name: /vorräte/i })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /vorräte/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user