Haal pakketten-data op via /api/v1/packages i.p.v. hardcoded content
Continuous Integration / config (pull_request) Successful in 3m1s
Continuous Integration / prepare (pull_request) Successful in 1m13s
Continuous Integration / build-production (pull_request) Skipped
Continuous Integration / build (pull_request) Successful in 2m14s
Continuous Integration / test (pull_request) Successful in 1m52s
Continuous Integration / deploy-production (pull_request) Skipped
Deploy / deploy (pull_request) Successful in 43s
Continuous Integration / deploy-test (pull_request) Successful in 44s

PackagesSection gebruikte tot nu toe statische data via een placeholder
query-hook (FR-5). usePackagesQuery haalt nu echt op bij /api/v1/packages
(relatief pad, zelfde domein op test en productie), met een foutmelding
in de UI als het request faalt.

Voegt ook een hand-off-document toe voor de agent die de CMS/API-kant
gaat bouwen: het endpoint-contract, per veld het type/gebruik, en de
huidige statische content als seed-data.
This commit is contained in:
2026-08-01 02:12:31 +02:00
parent 32a5bc0185
commit 7fc06091a5
7 changed files with 181 additions and 19 deletions
@@ -1,11 +1,13 @@
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { PackagesSection } from '../components/PackagesSection';
import { packages } from '../data/content';
function renderWithQueryClient() {
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<PackagesSection />
@@ -13,14 +15,28 @@ function renderWithQueryClient() {
);
}
function mockFetchOnce(response: Response) {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(response),
);
}
describe('PackagesSection', () => {
it('renders one PackageCard per package once loaded', async () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('fetches packages from /api/v1/packages and renders one PackageCard per package', async () => {
mockFetchOnce(new Response(JSON.stringify(packages), { status: 200 }));
renderWithQueryClient();
await waitFor(() => {
expect(screen.getByTestId('packages-grid').children.length).toBe(packages.length);
});
expect(fetch).toHaveBeenCalledWith('/api/v1/packages');
for (const pkg of packages) {
expect(screen.getByTestId(`package-card-${pkg.id}`)).toBeInTheDocument();
expect(screen.getByText(pkg.title)).toBeInTheDocument();
@@ -28,10 +44,22 @@ describe('PackagesSection', () => {
});
it('marks the featured package as "Meest gekozen"', async () => {
mockFetchOnce(new Response(JSON.stringify(packages), { status: 200 }));
renderWithQueryClient();
await waitFor(() => {
expect(screen.getByText('Meest gekozen')).toBeInTheDocument();
});
});
it('shows an error message when the API request fails', async () => {
mockFetchOnce(new Response(null, { status: 500, statusText: 'Internal Server Error' }));
renderWithQueryClient();
await waitFor(() => {
expect(screen.getByTestId('packages-error')).toBeInTheDocument();
});
});
});
@@ -2,7 +2,7 @@ import { usePackagesQuery } from '../hooks/usePackagesQuery';
import { PackageCard } from './PackageCard';
export function PackagesSection() {
const { data: packages, isLoading } = usePackagesQuery();
const { data: packages, isLoading, isError } = usePackagesQuery();
return (
<section
@@ -23,9 +23,13 @@ export function PackagesSection() {
</p>
</div>
<div className="grid grid-cols-1 gap-[22px] sm:grid-cols-3" data-testid="packages-grid">
{isLoading || !packages
? null
: packages.map((pkg) => <PackageCard key={pkg.id} pkg={pkg} />)}
{isError ? (
<p className="col-span-full text-center text-muted" data-testid="packages-error">
Pakketten konden niet worden geladen. Probeer het later opnieuw.
</p>
) : isLoading || !packages ? null : (
packages.map((pkg) => <PackageCard key={pkg.id} pkg={pkg} />)
)}
</div>
</div>
</section>
+5 -3
View File
@@ -5,9 +5,11 @@
* Shapes mirror the domain entities defined in
* aidlc-docs/features/react-frontend/construction/react-frontend-app/functional-design/domain-entities.md
*
* This module is the current source of truth for content (requirements FR-1/FR-6).
* It is deliberately structured so it can be swapped for TanStack Query data later
* (see src/features/landing/hooks/usePackagesQuery.ts) with minimal changes (requirements FR-5).
* This module is the source of truth for content that isn't (yet) API-backed
* (requirements FR-1/FR-6). `packages` is the exception: it now also doubles as
* the reference/seed data for the `/api/v1/packages` contract consumed via
* src/features/landing/hooks/usePackagesQuery.ts (requirements FR-5) — see
* aidlc-docs/features/react-frontend/construction/react-frontend-app/functional-design/packages-api-handoff.md.
*/
export interface NavLink {
+12 -8
View File
@@ -1,14 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { packages, type PackageCardData } from '../data/content';
import type { PackageCardData } from '../data/content';
// Relative path so it resolves against whichever domain served the app
// (test.slpsoftware.nl / slpsoftware.nl) — see aidlc-docs/features/react-frontend/
// construction/react-frontend-app/functional-design/packages-api-handoff.md
// for the API contract this endpoint must implement.
const PACKAGES_ENDPOINT = '/api/v1/packages';
/**
* Placeholder query hook (requirements FR-5). The queryFn currently just resolves the
* static package data, wrapped in a Promise so the calling component already consumes
* it exactly the way it will once a real API exists — only this queryFn's body will
* need to change in a future iteration.
*/
async function fetchPackages(): Promise<PackageCardData[]> {
return Promise.resolve(packages);
const response = await fetch(PACKAGES_ENDPOINT);
if (!response.ok) {
throw new Error(`Failed to fetch packages: ${response.status} ${response.statusText}`);
}
return response.json();
}
export function usePackagesQuery() {