Voeg loading-skeleton toe voor pakketten-sectie
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / prepare (pull_request) Successful in 1m36s
Continuous Integration / build-production (pull_request) Skipped
Continuous Integration / build (pull_request) Successful in 2m6s
Continuous Integration / test (pull_request) Successful in 2m5s
Continuous Integration / deploy-production (pull_request) Skipped
Deploy / deploy (pull_request) Successful in 35s
Continuous Integration / deploy-test (pull_request) Successful in 36s
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / prepare (pull_request) Successful in 1m36s
Continuous Integration / build-production (pull_request) Skipped
Continuous Integration / build (pull_request) Successful in 2m6s
Continuous Integration / test (pull_request) Successful in 2m5s
Continuous Integration / deploy-production (pull_request) Skipped
Deploy / deploy (pull_request) Successful in 35s
Continuous Integration / deploy-test (pull_request) Successful in 36s
Toont 3 pulserende placeholder-kaarten (zelfde afmetingen/structuur als PackageCard) zolang de offerings-fetch nog bezig is, zodat de sectie niet leeg oogt en de layout niet verspringt zodra de echte data binnenkomt.
This commit is contained in:
@@ -40,10 +40,11 @@ describe('PackagesSection', () => {
|
||||
renderWithQueryClient();
|
||||
|
||||
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(screen.getByTestId('packages-grid').children.length).toBe(packages.length);
|
||||
for (const pkg of packages) {
|
||||
expect(screen.getByTestId(`package-card-${pkg.id}`)).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 () => {
|
||||
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 { 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() {
|
||||
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">
|
||||
Pakketten konden niet worden geladen. Probeer het later opnieuw.
|
||||
</p>
|
||||
) : isLoading || !packages ? null : (
|
||||
) : isLoading || !packages ? (
|
||||
Array.from({ length: SKELETON_COUNT }, (_, i) => <PackageCardSkeleton key={i} />)
|
||||
) : (
|
||||
packages.map((pkg) => <PackageCard key={pkg.id} pkg={pkg} />)
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user