diff --git a/.gitea/workflows/continuous_integration.yaml b/.gitea/workflows/continuous_integration.yaml index 22aa7ec..4fd9a88 100644 --- a/.gitea/workflows/continuous_integration.yaml +++ b/.gitea/workflows/continuous_integration.yaml @@ -20,6 +20,17 @@ on: push: branches: [master] +# Annuleert een lopende workflow-run voor dezelfde PR zodra er een nieuwe push +# naar die PR gebeurt (bv. een extra commit tijdens review) - zo draait de CI +# niet nodeloos door op een verouderde commit. De group-key valt terug op +# github.run_id (altijd uniek) voor niet-PR events (push naar master, +# workflow_dispatch), zodat die runs elkaar NOOIT annuleren - met name de +# automatische test-deploy bij een push naar master mag niet halverwege +# afgebroken worden door een snel daaropvolgende tweede push. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + # Herbruikbare instellingen voor deze workflow. Pas deze aan op één plek als # de Node/pnpm-versie, de artifact-naam/pad of de testdeploy-bestemming # wijzigt. Let op: de env-context is NIET beschikbaar in de `with:`-inputs diff --git a/src/features/landing/__tests__/PackagesSection.test.tsx b/src/features/landing/__tests__/PackagesSection.test.tsx index 04ba12c..0c14b66 100644 --- a/src/features/landing/__tests__/PackagesSection.test.tsx +++ b/src/features/landing/__tests__/PackagesSection.test.tsx @@ -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((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' })); diff --git a/src/features/landing/components/PackageCardSkeleton.tsx b/src/features/landing/components/PackageCardSkeleton.tsx new file mode 100644 index 0000000..0fa4d37 --- /dev/null +++ b/src/features/landing/components/PackageCardSkeleton.tsx @@ -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 ( +