test: Cypress en Playwright e2e-suites voor login, agenda, cortex en verpleegrapportage
Configs voor beide runners, login via session caching, AI-chat mocks (SSE) voor deterministische Cortex-tests. Supabase local config toegevoegd. Auth-state en reports zijn gitignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
120
tests/playwright/agenda/agenda.spec.ts
Normal file
120
tests/playwright/agenda/agenda.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const MOCK_PATIENT = {
|
||||
id: 'test-patient-uuid',
|
||||
name_family: 'Robot',
|
||||
name_given: ['Robbert'],
|
||||
birth_date: '1990-01-01',
|
||||
identifier_client_number: '99001',
|
||||
};
|
||||
|
||||
const FHIR_PATIENT_RESPONSE = {
|
||||
entry: [
|
||||
{
|
||||
resource: {
|
||||
id: MOCK_PATIENT.id,
|
||||
name: [{ family: MOCK_PATIENT.name_family, given: MOCK_PATIENT.name_given }],
|
||||
birthDate: MOCK_PATIENT.birth_date,
|
||||
identifier: [
|
||||
{ system: 'http://example.com/client/999.7.6', value: MOCK_PATIENT.identifier_client_number },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test.describe('Agenda', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/epd/agenda');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('laadt de agendapagina', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Agenda' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('toont navigatieknoppen en weergave-switcher', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'Vandaag' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Dag' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Afspraak maken', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Mock patient search
|
||||
await page.route('**/api/fhir/Patient**', async (route) => {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FHIR_PATIENT_RESPONSE) });
|
||||
});
|
||||
|
||||
// Mock afspraak aanmaken (voorkom echte database write)
|
||||
await page.route('**/epd/agenda**', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true }) });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/epd/agenda');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('opent de "Nieuwe Afspraak" modal', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i }).click();
|
||||
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Nieuwe Afspraak' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('modal toont alle verplichte velden', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog.getByPlaceholder(/Zoek op naam/i)).toBeVisible();
|
||||
await expect(dialog.locator('input[type="date"]')).toBeVisible();
|
||||
await expect(dialog.locator('input[type="time"]').first()).toBeVisible();
|
||||
await expect(dialog.getByRole('button', { name: 'Afspraak maken' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('zoeken naar patiënt toont resultaat', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByPlaceholder(/Zoek op naam/i).fill('Robbert');
|
||||
|
||||
// Dropdown met patiënt verschijnt
|
||||
await expect(dialog.getByText('Robbert Robot')).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
|
||||
test('selecteren van patiënt vult het veld in', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByPlaceholder(/Zoek op naam/i).fill('Robbert');
|
||||
await expect(dialog.getByText('Robbert Robot')).toBeVisible({ timeout: 5_000 });
|
||||
await dialog.getByText('Robbert Robot').click();
|
||||
|
||||
// Patiënt bevestigingskaart zichtbaar
|
||||
await expect(dialog.locator('.bg-teal-50')).toBeVisible();
|
||||
});
|
||||
|
||||
test('afspraak maken knop is uitgeschakeld zonder patiënt', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
const submitButton = dialog.getByRole('button', { name: 'Afspraak maken' });
|
||||
|
||||
await expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('sluiten sluit de modal', async ({ page }) => {
|
||||
await page.getByRole('button', { name: /Nieuwe Afspraak|Nieuw/i }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await dialog.getByRole('button', { name: 'Sluiten' }).click();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
49
tests/playwright/auth.setup.ts
Normal file
49
tests/playwright/auth.setup.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test as setup } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
const authFile = path.join(process.cwd(), 'tests/playwright/.auth/user.json');
|
||||
|
||||
const DEMO_EMAIL = process.env.TEST_EMAIL ?? 'demo@mini-ecd.demo';
|
||||
const DEMO_PASSWORD = process.env.TEST_PASSWORD ?? 'Demo2024!';
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
setup('authenticate as demo user', async ({ page, request }) => {
|
||||
// Authenticate via Supabase REST API - faster and more reliable than UI form
|
||||
const response = await request.post(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, {
|
||||
headers: {
|
||||
apikey: SUPABASE_ANON_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { email: DEMO_EMAIL, password: DEMO_PASSWORD },
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(`Supabase auth failed: ${response.status()} ${await response.text()}`);
|
||||
}
|
||||
|
||||
const tokenData = await response.json();
|
||||
|
||||
// Build Supabase auth cookie (format: base64-<base64 encoded JSON>)
|
||||
const projectRef = SUPABASE_URL.replace('https://', '').split('.')[0];
|
||||
const cookieName = `sb-${projectRef}-auth-token`;
|
||||
const cookieValue = `base64-${Buffer.from(JSON.stringify(tokenData)).toString('base64')}`;
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: cookieName,
|
||||
value: cookieValue,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
]);
|
||||
|
||||
// Verify the session works by navigating to a protected route
|
||||
await page.goto('/epd/cortex');
|
||||
await page.waitForURL(/\/epd\//, { timeout: 15_000 });
|
||||
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
39
tests/playwright/cortex/command-center.spec.ts
Normal file
39
tests/playwright/cortex/command-center.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
// De pagina heeft twee textareas (desktop + mobiel layout). .first() pakt de zichtbare desktop-versie.
|
||||
const input = (page: Page) => page.getByPlaceholder('Typ of spreek wat je wilt doen...').first();
|
||||
|
||||
test.describe('Cortex Command Center - UI', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/epd/cortex');
|
||||
await expect(input(page)).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('laadt de command center interface', async ({ page }) => {
|
||||
await expect(input(page)).toBeVisible();
|
||||
await expect(page.locator('text=Welkom bij Cortex Assistent').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('chat input accepteert tekst', async ({ page }) => {
|
||||
await input(page).fill('test invoer');
|
||||
await expect(input(page)).toHaveValue('test invoer');
|
||||
});
|
||||
|
||||
test('keyboard shortcut ⌘K focust de input', async ({ page }) => {
|
||||
await page.click('body');
|
||||
await page.keyboard.press('Meta+k');
|
||||
await expect(input(page)).toBeFocused();
|
||||
});
|
||||
|
||||
test('escape wist de input', async ({ page }) => {
|
||||
await input(page).fill('iets typen');
|
||||
await input(page).press('Escape');
|
||||
await expect(input(page)).toHaveValue('');
|
||||
});
|
||||
|
||||
test('toont voorbeeldcommandos in de empty state', async ({ page }) => {
|
||||
await expect(page.locator('text=Dagnotitie maken').first()).toBeVisible();
|
||||
await expect(page.locator('text=Patiënt zoeken').first()).toBeVisible();
|
||||
await expect(page.locator('text=Overdracht maken').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
63
tests/playwright/cortex/dagnotitie-flow.spec.ts
Normal file
63
tests/playwright/cortex/dagnotitie-flow.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { mockDagnotitieChat, mockZoekenChat, mockOverdrachtChat, mockOnbekendChat } from '../fixtures/ai-mocks';
|
||||
|
||||
// De pagina heeft twee textareas (desktop + mobiel layout). .first() pakt de zichtbare desktop-versie.
|
||||
const input = (page: Page) => page.getByPlaceholder('Typ of spreek wat je wilt doen...').first();
|
||||
|
||||
test.describe('Cortex - Demo flows', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/epd/cortex');
|
||||
await expect(input(page)).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('dagnotitie flow: bericht → AI response → artifact verschijnt', async ({ page }) => {
|
||||
await mockDagnotitieChat(page, 'Jan de Vries');
|
||||
await input(page).fill('notitie jan de vries voelt zich goed vandaag');
|
||||
await input(page).press('Enter');
|
||||
await expect(page.locator('text=notitie jan de vries voelt zich goed vandaag').first()).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.locator('text=Genoteerd.').first()).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('dagnotitie flow: input is leeg na verzenden', async ({ page }) => {
|
||||
await mockDagnotitieChat(page);
|
||||
await input(page).fill('notitie jan medicatie gegeven');
|
||||
await input(page).press('Enter');
|
||||
await expect(input(page)).toHaveValue('', { timeout: 3_000 });
|
||||
});
|
||||
|
||||
test('zoeken flow: bericht → zoek-artifact verschijnt', async ({ page }) => {
|
||||
await mockZoekenChat(page, 'Marie');
|
||||
await input(page).fill('zoek marie');
|
||||
await input(page).press('Enter');
|
||||
await expect(page.locator('text=zoek marie').first()).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.locator('text=Even kijken.').first()).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('overdracht flow: bericht → overdracht-artifact', async ({ page }) => {
|
||||
await mockOverdrachtChat(page);
|
||||
await input(page).fill('overdracht');
|
||||
await input(page).press('Enter');
|
||||
await expect(page.locator('text=overdracht').first()).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.locator('text=Overdracht klaar.').first()).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('onbekende intent: AI vraagt om verduidelijking', async ({ page }) => {
|
||||
await mockOnbekendChat(page);
|
||||
await input(page).fill('xyz');
|
||||
await input(page).press('Enter');
|
||||
await expect(page.locator('text=snap niet helemaal wat je bedoelt').first()).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('meerdere berichten: chat history groeit', async ({ page }) => {
|
||||
await mockDagnotitieChat(page);
|
||||
await input(page).fill('notitie jan medicatie gegeven');
|
||||
await input(page).press('Enter');
|
||||
await expect(page.locator('text=Genoteerd.').first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await mockDagnotitieChat(page, 'Marie');
|
||||
await input(page).fill('notitie marie slaapt slecht');
|
||||
await input(page).press('Enter');
|
||||
await expect(page.locator('text=notitie jan medicatie gegeven').first()).toBeVisible();
|
||||
await expect(page.locator('text=notitie marie slaapt slecht').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
110
tests/playwright/fixtures/ai-mocks.ts
Normal file
110
tests/playwright/fixtures/ai-mocks.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Builds a mock SSE response body for the Cortex chat endpoint.
|
||||
* The chat route streams: { type: 'content', text } chunks, then { type: 'done' }.
|
||||
*/
|
||||
function buildSSEBody(text: string, actionJson?: object): string {
|
||||
const encoder = new TextEncoder();
|
||||
const parts: string[] = [];
|
||||
|
||||
// Stream text in one chunk for simplicity
|
||||
const fullText = actionJson
|
||||
? `${text}\n\n\`\`\`json\n${JSON.stringify(actionJson, null, 2)}\n\`\`\``
|
||||
: text;
|
||||
|
||||
parts.push(`data: ${JSON.stringify({ type: 'content', text: fullText })}\n\n`);
|
||||
parts.push(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/** Mock de Cortex chat endpoint voor een dagnotitie intent. */
|
||||
export async function mockDagnotitieChat(page: Page, patientName = 'Jan de Vries') {
|
||||
await page.route('**/api/cortex/chat', async (route) => {
|
||||
const action = {
|
||||
type: 'action',
|
||||
intent: 'dagnotitie',
|
||||
entities: {
|
||||
patientName,
|
||||
category: 'observatie',
|
||||
content: 'Patiënt voelt zich goed vandaag.',
|
||||
},
|
||||
confidence: 0.95,
|
||||
artifact: {
|
||||
type: 'dagnotitie',
|
||||
prefill: {
|
||||
patientName,
|
||||
category: 'observatie',
|
||||
content: 'Patiënt voelt zich goed vandaag.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/event-stream',
|
||||
body: buildSSEBody('Genoteerd.', action),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Mock de Cortex chat endpoint voor een zoeken intent. */
|
||||
export async function mockZoekenChat(page: Page, query = 'Jan') {
|
||||
await page.route('**/api/cortex/chat', async (route) => {
|
||||
const action = {
|
||||
type: 'action',
|
||||
intent: 'zoeken',
|
||||
entities: { query },
|
||||
confidence: 0.97,
|
||||
artifact: {
|
||||
type: 'zoeken',
|
||||
prefill: { query },
|
||||
},
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/event-stream',
|
||||
body: buildSSEBody('Even kijken.', action),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Mock de Cortex chat endpoint voor een overdracht intent. */
|
||||
export async function mockOverdrachtChat(page: Page) {
|
||||
await page.route('**/api/cortex/chat', async (route) => {
|
||||
const action = {
|
||||
type: 'action',
|
||||
intent: 'overdracht',
|
||||
entities: {},
|
||||
confidence: 0.98,
|
||||
artifact: {
|
||||
type: 'overdracht',
|
||||
prefill: {},
|
||||
},
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/event-stream',
|
||||
body: buildSSEBody('Overdracht klaar.', action),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Mock de Cortex chat endpoint voor een onduidelijke intent (geen artifact). */
|
||||
export async function mockOnbekendChat(page: Page) {
|
||||
await page.route('**/api/cortex/chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/event-stream',
|
||||
body: buildSSEBody('Hmm, snap niet helemaal wat je bedoelt. Kun je het anders zeggen?'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Verwijder alle actieve route mocks op de chat endpoint. */
|
||||
export async function clearChatMocks(page: Page) {
|
||||
await page.unroute('**/api/cortex/chat');
|
||||
}
|
||||
56
tests/playwright/login.spec.ts
Normal file
56
tests/playwright/login.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// These tests run WITHOUT storageState (they test the login UI itself)
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test.describe('Login flow', () => {
|
||||
test('toont de loginpagina correct', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await expect(page.locator('h2')).toContainText('Welkom terug');
|
||||
await expect(page.locator('#email')).toBeVisible();
|
||||
await expect(page.locator('#password')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('toont foutmelding bij verkeerde inloggegevens', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('#email', 'verkeerd@email.nl');
|
||||
await page.fill('#password', 'foutWachtwoord123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for error message
|
||||
await expect(page.locator('text=onjuist')).toBeVisible({ timeout: 8_000 });
|
||||
});
|
||||
|
||||
test('demo login knop is zichtbaar en werkt', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
const demoButton = page.getByRole('button', { name: /Demo Account Proberen/i });
|
||||
await expect(demoButton).toBeVisible();
|
||||
|
||||
await demoButton.click();
|
||||
|
||||
// Should redirect to EPD after demo login
|
||||
await page.waitForURL(/\/epd\//, { timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('redirect naar EPD na succesvol inloggen', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await page.fill('#email', 'demo@mini-ecd.demo');
|
||||
await page.fill('#password', 'Demo2024!');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL(/\/epd\//, { timeout: 10_000 });
|
||||
expect(page.url()).toMatch(/\/epd\//);
|
||||
});
|
||||
|
||||
test('beveiligde route stuurt door naar login', async ({ page }) => {
|
||||
await page.goto('/epd/dashboard');
|
||||
|
||||
await page.waitForURL('/login**', { timeout: 5_000 });
|
||||
expect(page.url()).toContain('/login');
|
||||
});
|
||||
});
|
||||
108
tests/playwright/patients/rapportage.spec.ts
Normal file
108
tests/playwright/patients/rapportage.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const PATIENT_NAAM = 'Robbert Robot';
|
||||
const AUTH_STATE = 'tests/playwright/.auth/user.json';
|
||||
|
||||
const RAPPORTAGES = [
|
||||
{
|
||||
type: 'Voortgang',
|
||||
inhoud:
|
||||
'Cliënt heeft vandaag goed geslapen en voelt zich uitgerust. Medicatie correct ingenomen zonder bijwerkingen. Stemming is stabiel en positief gedurende de gehele dag.',
|
||||
},
|
||||
{
|
||||
type: 'Contact',
|
||||
inhoud:
|
||||
'Telefonisch contact gehad met de familie van de cliënt. Positieve terugkoppeling over het afgelopen weekend. Familie ervaart momenteel weinig zorgen over de thuissituatie.',
|
||||
},
|
||||
{
|
||||
type: 'Vrije notitie',
|
||||
inhoud:
|
||||
'Cliënt heeft interesse getoond in deelname aan de dagactiviteitengroep. Dit voorstel wordt volgende week besproken tijdens het multidisciplinair overleg met het behandelteam.',
|
||||
},
|
||||
];
|
||||
|
||||
test.describe(`Rapportages schrijven - ${PATIENT_NAAM}`, () => {
|
||||
let rapportageUrl: string;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
// Gebruik een aparte context om het patiënt-ID op te zoeken
|
||||
const context = await browser.newContext({ storageState: AUTH_STATE });
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/epd/patients');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Zoek Robbert Robot via de zoekbalk
|
||||
await page.getByPlaceholder('Zoek op naam of BSN...').fill('Robbert');
|
||||
await expect(page.getByText(PATIENT_NAAM).first()).toBeVisible({ timeout: 8_000 });
|
||||
await page.getByText(PATIENT_NAAM).first().click();
|
||||
await page.waitForURL(/\/epd\/patients\/.+/);
|
||||
|
||||
const patientId = page.url().match(/\/epd\/patients\/([^/?#]+)/)?.[1];
|
||||
if (!patientId) throw new Error(`Patiënt "${PATIENT_NAAM}" niet gevonden in de patiëntenlijst`);
|
||||
|
||||
rapportageUrl = `/epd/patients/${patientId}/rapportage`;
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(rapportageUrl);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wacht tot de TipTap editor volledig geladen is
|
||||
await expect(page.locator('.ProseMirror')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Verwijder eventuele localStorage drafts voor een schone start
|
||||
await page.evaluate(() => {
|
||||
Object.keys(localStorage)
|
||||
.filter((k) => k.startsWith('rapportage-draft-'))
|
||||
.forEach((k) => localStorage.removeItem(k));
|
||||
});
|
||||
});
|
||||
|
||||
RAPPORTAGES.forEach((rapportage, i) => {
|
||||
test(`rapportage ${i + 1}: ${rapportage.type}`, async ({ page }) => {
|
||||
// Selecteer het rapportage type via de QuickActions (exact=true voorkomt match met tijdlijn items)
|
||||
await page.getByRole('button', { name: rapportage.type, exact: true }).click();
|
||||
|
||||
// Klik op de editor en typ de inhoud
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await page.keyboard.type(rapportage.inhoud);
|
||||
|
||||
// Verifieer dat de tekst meetelt in de karakterteller
|
||||
await expect(page.locator('text=/\\d+ \\/ 5000 karakters/')).toBeVisible();
|
||||
|
||||
// Opslaan knop moet actief zijn (inhoud > 20 karakters)
|
||||
const opslaanKnop = page.getByRole('button', { name: 'Opslaan' });
|
||||
await expect(opslaanKnop).toBeEnabled();
|
||||
await opslaanKnop.click();
|
||||
|
||||
// Verifieer de succesmelding
|
||||
await expect(page.getByText('Rapportage opgeslagen').first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Na opslaan is de editor leeg → Opslaan knop is weer uitgeschakeld
|
||||
await expect(opslaanKnop).toBeDisabled({ timeout: 5_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('alle drie rapportages staan in de tijdlijn', async ({ page }) => {
|
||||
// Schrijf de drie rapportages in volgorde
|
||||
for (const rapportage of RAPPORTAGES) {
|
||||
await page.getByRole('button', { name: rapportage.type, exact: true }).click();
|
||||
await page.locator('.ProseMirror').click();
|
||||
await page.keyboard.type(rapportage.inhoud);
|
||||
await page.getByRole('button', { name: 'Opslaan', exact: true }).click();
|
||||
await expect(page.getByText('Rapportage opgeslagen').first()).toBeVisible({ timeout: 10_000 });
|
||||
// exact: true onderscheidt "Opslaan" (klaar) van "Opslaan…" (bezig) — wacht tot opslaan echt klaar is
|
||||
await expect(page.getByRole('button', { name: 'Opslaan', exact: true })).toBeDisabled({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
// Controleer dat de tijdlijn de rapportages toont
|
||||
// TimelineCard rendert als <article role="button"> — anders dan QuickAction <button> elementen
|
||||
const tijdlijnKaarten = page.locator('article[role="button"]');
|
||||
await expect(tijdlijnKaarten.filter({ hasText: 'Voortgang' }).first()).toBeVisible({ timeout: 5_000 });
|
||||
await expect(tijdlijnKaarten.filter({ hasText: 'Contact' }).first()).toBeVisible({ timeout: 5_000 });
|
||||
await expect(tijdlijnKaarten.filter({ hasText: 'Vrije notitie' }).first()).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
});
|
||||
38
tests/playwright/verpleegrapportage/overzicht.spec.ts
Normal file
38
tests/playwright/verpleegrapportage/overzicht.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Verpleegrapportage', () => {
|
||||
test('laadt de rapportage pagina', async ({ page }) => {
|
||||
await page.goto('/epd/verpleegrapportage');
|
||||
|
||||
// Pagina laadt zonder fouten (geen 404/500)
|
||||
await expect(page).not.toHaveURL(/\/login/);
|
||||
|
||||
// Wacht op content
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Pagina is zichtbaar (lege state of workspace)
|
||||
const body = page.locator('body');
|
||||
await expect(body).toBeVisible();
|
||||
});
|
||||
|
||||
test('toont lege staat of patiëntenlijst', async ({ page }) => {
|
||||
await page.goto('/epd/verpleegrapportage');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Workspace toont "Ronde overzicht" als er patiënten zijn, anders lege staat
|
||||
const hasWorkspace = await page.locator('text=Ronde overzicht').isVisible().catch(() => false);
|
||||
const hasEmptyState = await page.locator('text=Geen patiënten').isVisible().catch(() => false);
|
||||
|
||||
expect(hasWorkspace || hasEmptyState).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Overdracht pagina', () => {
|
||||
test('laadt de overdracht pagina', async ({ page }) => {
|
||||
await page.goto('/epd/verpleegrapportage/overdracht');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page).not.toHaveURL(/\/login/);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user