feat(fp): WP-19 — Playwright e2e smoke against the real FE+backend
Adds a happy-path spec (login → dashboard → registratie wizard, including a real identity-document upload → real submit) and a degraded-path spec (?scenario=error → <app-async> error slot → retry), both driving the real app against the real .NET backend, plus a CI job that boots both. Writing the retry spec surfaced a real bug: AsyncComponent's retry() only reloads a [resource]-fed instance, so every real page (all [data]-fed via a store's RemoteData) had a silently no-op retry button. Added a retryClicked output and wired it on the dashboard's two async blocks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
32
e2e/error-state.spec.ts
Normal file
32
e2e/error-state.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// The dev-only `?scenario=error` toggle forces the scenario.interceptor to fail
|
||||
// the request WITHOUT ever reaching the real HTTP transport (it substitutes a
|
||||
// `throwError` in the rxjs pipe before `next(req)` runs) — so this is not
|
||||
// observable as a browser network request. It IS observable as a real resource
|
||||
// reload: `retry()` calls `resource.reload()`, which flips <app-async> back to
|
||||
// its Loading (`aria-busy="true"`) state before failing again 400ms later.
|
||||
// `currentScenario()` re-reads `window.location.search` on every call, not a
|
||||
// one-shot value, so as long as the query param is still there, the retry
|
||||
// genuinely re-runs and genuinely fails the same way — that's what's asserted
|
||||
// here: a real reload cycle, not a no-op button.
|
||||
test('dashboard error state renders, retry re-fetches (and fails again)', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('BSN').fill('123456789');
|
||||
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
|
||||
await page.goto('/dashboard?scenario=error');
|
||||
// The dashboard has more than one independent <app-async> (profile,
|
||||
// aantekeningen) — both fail under the blanket ?scenario=error, so this text
|
||||
// legitimately appears more than once. Assert at least one, not exactly one.
|
||||
const errorAlert = page.getByText('Er ging iets mis bij het laden van de gegevens.').first();
|
||||
await expect(errorAlert).toBeVisible();
|
||||
const retry = page.getByRole('button', { name: 'Opnieuw proberen' }).first();
|
||||
await expect(retry).toBeVisible();
|
||||
|
||||
await retry.click();
|
||||
// A real reload cycle: back to Loading (aria-busy) before failing again.
|
||||
await expect(page.locator('[aria-busy="true"]').first()).toBeAttached({ timeout: 1_000 });
|
||||
await expect(errorAlert).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
59
e2e/smoke.spec.ts
Normal file
59
e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// One happy-path flow through the real FE+backend: log in, land on the real
|
||||
// dashboard, run the registratie wizard's minimum required path (a DUO diploma
|
||||
// with zero policy questions, so the only required upload is identiteit), submit,
|
||||
// and see the real confirmation. Not a full wizard-coverage suite — see WP-19.
|
||||
//
|
||||
// The backend is in-memory and shared across runs; this test mutates real state
|
||||
// (creates a registratie application for the fixed demo identity). Restart the
|
||||
// backend between CI runs — a second run would see a leftover Concept/submitted
|
||||
// application on the dashboard, which this test doesn't assert against, but a
|
||||
// stricter future test might.
|
||||
test('login → dashboard → registratie wizard → submitted', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('BSN').fill('123456789');
|
||||
await page.getByLabel('Wachtwoord').fill('demo');
|
||||
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
await expect(page.getByRole('heading', { level: 1, name: 'Mijn overzicht' })).toBeVisible();
|
||||
// Real backend content, not a loading/error state.
|
||||
await expect(page.getByText('Persoonsgegevens (BRP)')).toBeVisible();
|
||||
|
||||
await page.goto('/registreren');
|
||||
await expect(
|
||||
page.getByRole('heading', { level: 1, name: 'Inschrijven in het BIG-register' }),
|
||||
).toBeVisible();
|
||||
|
||||
// Step 1 — adres: BRP prefill is real backend data; "Post" skips the email field.
|
||||
// The CIBG-styled radio hides the native input behind its label, so click the
|
||||
// label (real user interaction) rather than `.check()` the covered input.
|
||||
await expect(page.getByText('Vooraf ingevuld op basis van de BRP')).toBeVisible();
|
||||
await page.locator('label[for="correspondentie-post"]').click();
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Step 2 — beroep: the first DUO diploma (Geneeskunde, non-English) carries zero
|
||||
// policy questions, so the only required document is identiteit.
|
||||
await expect(page.locator('#diploma-d1')).toBeVisible({ timeout: 10_000 });
|
||||
await page.locator('label[for="diploma-d1"]').click();
|
||||
await expect(page.getByText('Beroep (afgeleid uit diploma)')).toBeVisible();
|
||||
|
||||
await page.locator('#identiteit-file').setInputFiles({
|
||||
name: 'identiteit.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: Buffer.from('%PDF-1.4 fake e2e fixture'),
|
||||
});
|
||||
// Real upload against the backend: wait for the row to leave "uploading".
|
||||
await expect(page.locator('.upload-progress')).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(page.locator('.icon-remove')).toBeVisible();
|
||||
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Step 3 — controle: review + real submit.
|
||||
await expect(page.getByText('Controleer uw gegevens en dien de registratie in.')).toBeVisible();
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
await expect(page.getByText('Uw registratie is ontvangen')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText(/Uw referentienummer is/)).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user