Voeg loading-skeleton toe voor pakketten-sectie #13
@@ -40,10 +40,11 @@ describe('PackagesSection', () => {
|
|||||||
renderWithQueryClient();
|
renderWithQueryClient();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('packages-grid').children.length).toBe(packages.length);
|
expect(screen.queryByTestId('package-card-skeleton')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith('/api/v1/offerings');
|
expect(fetch).toHaveBeenCalledWith('/api/v1/offerings');
|
||||||
|
expect(screen.getByTestId('packages-grid').children.length).toBe(packages.length);
|
||||||
for (const pkg of packages) {
|
for (const pkg of packages) {
|
||||||
expect(screen.getByTestId(`package-card-${pkg.id}`)).toBeInTheDocument();
|
expect(screen.getByTestId(`package-card-${pkg.id}`)).toBeInTheDocument();
|
||||||
expect(screen.getByText(pkg.title)).toBeInTheDocument();
|
expect(screen.getByText(pkg.title)).toBeInTheDocument();
|
||||||
@@ -60,6 +61,29 @@ describe('PackagesSection', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows skeleton placeholders while the request is in flight, reserving the grid space', async () => {
|
||||||
|
let resolveFetch: (response: Response) => void;
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockReturnValue(
|
||||||
|
new Promise<Response>((resolve) => {
|
||||||
|
resolveFetch = resolve;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderWithQueryClient();
|
||||||
|
|
||||||
|
expect(screen.getAllByTestId('package-card-skeleton')).toHaveLength(packages.length);
|
||||||
|
|
||||||
|
resolveFetch!(new Response(JSON.stringify(packages), { status: 200 }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryAllByTestId('package-card-skeleton')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('packages-grid').children.length).toBe(packages.length);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows an error message when the API request fails', async () => {
|
it('shows an error message when the API request fails', async () => {
|
||||||
mockFetchOnce(new Response(null, { status: 500, statusText: 'Internal Server Error' }));
|
mockFetchOnce(new Response(null, { status: 500, statusText: 'Internal Server Error' }));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Mirrors PackageCard's structure/spacing 1-to-1 so the grid doesn't reflow once
|
||||||
|
// real data arrives — only the content becomes pulsing placeholder blocks.
|
||||||
|
export function PackageCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex animate-pulse flex-col rounded border border-line bg-surface p-8"
|
||||||
|
data-testid="package-card-skeleton"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div className="mb-3.5 h-[0.78rem] w-16 rounded bg-line" />
|
||||||
|
<div className="mb-2.5 h-6 w-3/4 rounded bg-line" />
|
||||||
|
<div className="mb-6 space-y-2">
|
||||||
|
<div className="h-4 w-full rounded bg-line" />
|
||||||
|
<div className="h-4 w-2/3 rounded bg-line" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 h-8 w-24 rounded bg-line" />
|
||||||
|
<div className="mb-6 h-[0.78rem] w-28 rounded bg-line" />
|
||||||
|
<ul className="my-6 flex-1 list-none space-y-2.5">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<li key={i} className="h-4 w-full rounded bg-line" />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="h-[3.4rem] w-full rounded-lg bg-line" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { usePackagesQuery } from '../hooks/usePackagesQuery';
|
import { usePackagesQuery } from '../hooks/usePackagesQuery';
|
||||||
import { PackageCard } from './PackageCard';
|
import { PackageCard } from './PackageCard';
|
||||||
|
import { PackageCardSkeleton } from './PackageCardSkeleton';
|
||||||
|
|
||||||
|
// Matches the fixed number of packages (see content.ts) so the skeleton grid has the
|
||||||
|
// same shape as the loaded grid — no layout shift once the real cards arrive.
|
||||||
|
const SKELETON_COUNT = 3;
|
||||||
|
|
||||||
export function PackagesSection() {
|
export function PackagesSection() {
|
||||||
const { data: packages, isLoading, isError } = usePackagesQuery();
|
const { data: packages, isLoading, isError } = usePackagesQuery();
|
||||||
@@ -27,7 +32,9 @@ export function PackagesSection() {
|
|||||||
<p className="col-span-full text-center text-muted" data-testid="packages-error">
|
<p className="col-span-full text-center text-muted" data-testid="packages-error">
|
||||||
Pakketten konden niet worden geladen. Probeer het later opnieuw.
|
Pakketten konden niet worden geladen. Probeer het later opnieuw.
|
||||||
</p>
|
</p>
|
||||||
) : isLoading || !packages ? null : (
|
) : isLoading || !packages ? (
|
||||||
|
Array.from({ length: SKELETON_COUNT }, (_, i) => <PackageCardSkeleton key={i} />)
|
||||||
|
) : (
|
||||||
packages.map((pkg) => <PackageCard key={pkg.id} pkg={pkg} />)
|
packages.map((pkg) => <PackageCard key={pkg.id} pkg={pkg} />)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user