import { test, expect, Page } from '@playwright/test'; import type { School } from '../src/app/core/api-models'; const backend = process.env['PLAYWRIGHT_BASE_URL'] || 'http://127.0.0.1:8080'; let schoolToDelete: number | undefined; test.afterEach(async ({ request }) => { if (schoolToDelete !== undefined) { await request.delete(`${backend}/api/dance-schools/${schoolToDelete}`); schoolToDelete = undefined; } }); async function tab(page: Page, label: string) { await page .getByRole('navigation', { name: 'Dansschool onderdelen' }) .getByRole('link', { name: new RegExp(`^${label}`) }) .click(); } async function save(page: Page) { await page.getByRole('dialog').getByRole('button', { name: 'Opslaan', exact: true }).click(); await expect(page.getByRole('dialog')).toHaveCount(0); } async function confirmDelete(page: Page) { await page .getByRole('dialog') .getByRole('button', { name: 'Definitief verwijderen', exact: true }) .click(); await expect(page.getByRole('dialog')).toHaveCount(0); } test('manages a school, shared memberships and shows through the real REST API', async ({ page, request, }) => { const schoolName = `E2E Studio ${Date.now()}`; const browserErrors: string[] = []; page.on('pageerror', (error) => browserErrors.push(error.message)); await page.goto('/'); await page.getByRole('button', { name: 'Dansschool toevoegen' }).click(); await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill(schoolName); const created = page.waitForResponse( (response) => response.url().endsWith('/api/dance-schools') && response.request().method() === 'POST', ); await save(page); const school: unknown = await (await created).json(); expect(school).toEqual(expect.objectContaining({ id: expect.any(Number) })); schoolToDelete = (school as School).id; await page.getByRole('link', { name: new RegExp(schoolName) }).click(); await expect(page.getByRole('heading', { name: schoolName, exact: true })).toBeVisible(); await page.getByRole('button', { name: 'School bewerken' }).click(); await page .getByRole('dialog') .getByLabel('Naam', { exact: true }) .fill(`${schoolName} vernieuwd`); await save(page); await expect( page.getByRole('heading', { name: `${schoolName} vernieuwd`, exact: true }), ).toBeVisible(); await page.getByRole('button', { name: /Voeg een danser toe/ }).click(); await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('Sam Testdanser'); await page.getByLabel('Geboortedatum').fill('2010-05-12'); await save(page); await tab(page, 'Dansers'); await expect(page.getByRole('cell', { name: 'Sam Testdanser', exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Sam Testdanser bewerken' }).click(); await page.getByLabel('Geboortedatum').fill('2011-06-13'); await save(page); await expect(page.getByRole('cell', { name: '13 jun 2011' })).toBeVisible(); await tab(page, 'Dansgroepen'); for (const name of ['Hiphop Teens', 'Jazz Teens']) { await page.getByRole('button', { name: 'Dansgroep toevoegen' }).click(); const dialog = page.getByRole('dialog'); await dialog.getByLabel('Naam / categorie').fill(name); await dialog.getByLabel('Leeftijd vanaf').fill('12'); await dialog.getByLabel('Leeftijd tot en met').fill('18'); await dialog.getByLabel(name.startsWith('Hiphop') ? 'Hiphop' : 'Jazz', { exact: true }).check(); await save(page); await page.getByRole('button', { name: `Dansers beheren voor ${name}`, exact: true }).click(); await page .getByRole('dialog') .locator('.member-row') .filter({ hasText: 'Sam Testdanser' }) .getByRole('checkbox') .check(); await expect(page.getByRole('dialog').getByText('1 van 1 dansers gekoppeld')).toBeVisible(); await page.getByRole('button', { name: 'Gereed', exact: true }).click(); await expect( page .getByRole('article') .filter({ has: page.getByRole('heading', { name, exact: true }) }) .getByText(/1 dansers/), ).toBeVisible(); } await page.getByRole('button', { name: 'Hiphop Teens bewerken' }).click(); await page.getByLabel('Naam / categorie').fill('Hiphop Selectie'); await page.getByLabel('Type', { exact: true }).selectOption({ label: 'Selectie' }); await save(page); await expect( page .getByRole('article') .filter({ hasText: 'Hiphop Selectie' }) .getByText(/1 dansers/), ).toBeVisible(); await page.screenshot({ path: 'test-results/groups-desktop.png', fullPage: true }); // Two independent performances give Sam time to change between the shared groups. for (const name of ['Modern adults', 'Streetdance adults']) { const dancerResponse = await request.post( `${backend}/api/dance-schools/${schoolToDelete}/dancers`, { data: { name, birthDate: '1990-01-01' }, }, ); const dancer = (await dancerResponse.json()) as { id: number }; const groupResponse = await request.post( `${backend}/api/dance-schools/${schoolToDelete}/dance-groups`, { data: { category: name, type: 'REGULAR', startAge: 0, endAge: 99, styles: ['MODERN'] }, }, ); const group = (await groupResponse.json()) as { id: number }; await request.put( `${backend}/api/dance-schools/${schoolToDelete}/dance-groups/${group.id}/dancers/${dancer.id}`, ); } await tab(page, 'Shows'); await page.getByRole('button', { name: 'Show toevoegen' }).click(); await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('Zomershow'); await page.getByRole('button', { name: 'Dagdeel toevoegen', exact: true }).click(); await page.getByLabel('Datum', { exact: true }).fill('2027-06-12'); await save(page); await expect(page.getByText('2027-06-12 ยท AFTERNOON:', { exact: false })).toBeVisible(); await page.getByRole('button', { name: 'Zomershow bewerken' }).click(); await page.getByLabel('Datum', { exact: true }).fill('2027-07-01'); await save(page); await expect(page.locator('.date-tile')).toContainText('jul 2027'); page.once('dialog', (dialog) => dialog.accept()); await page.getByRole('button', { name: /Schedule groups/ }).click(); await expect(page.getByText('Planning voldoet aan alle harde regels.')).toBeVisible(); await expect( page.locator('.scheduling-progress').filter({ hasText: 'Optimaal conceptrooster.' }), ).toBeVisible(); await expect(page.locator('.scheduling-snackbar')).toContainText('Zomershow'); await expect(page.locator('.scheduling-progress')).toContainText('Spreiding:'); await page.reload(); await expect(page.locator('.scheduling-progress')).toContainText('Optimaal conceptrooster.'); await expect(page.locator('.scheduling-snackbar')).toHaveCount(0); await page.getByRole('button', { name: 'Indeling en volgorde bewerken' }).click(); const scheduleDialog = page.getByRole('dialog'); await scheduleDialog.getByRole('button', { name: 'Definitief maken' }).click(); await expect(scheduleDialog.getByText('Definitief', { exact: false }).first()).toBeVisible(); await scheduleDialog.getByRole('button', { name: 'Optreden verwijderen' }).first().click(); await scheduleDialog.getByRole('button', { name: 'Concept opslaan' }).click(); await expect(scheduleDialog.locator('.alert.error').first()).toBeVisible(); await expect(scheduleDialog.getByRole('button', { name: 'Definitief maken' })).toBeDisabled(); await scheduleDialog.getByRole('button', { name: 'Gereed', exact: true }).click(); await page.getByRole('button', { name: 'Zomershow bewerken' }).click(); await page.getByRole('button', { name: 'Dagdeel toevoegen', exact: true }).click(); await page.getByLabel('Datum', { exact: true }).nth(1).fill('2027-07-02'); page.once('dialog', async (dialog) => { expect(dialog.message()).toContain('Zomershow'); await dialog.dismiss(); }); await page.getByRole('button', { name: 'Opslaan', exact: true }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: 'Annuleren', exact: true }).click(); await tab(page, 'Overzicht'); await page.setViewportSize({ width: 390, height: 844 }); await expect( page.getByRole('heading', { name: `${schoolName} vernieuwd`, exact: true }), ).toBeVisible(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe( true, ); await page.screenshot({ path: 'test-results/overview-mobile.png', fullPage: true }); await page.setViewportSize({ width: 1440, height: 1000 }); await page.screenshot({ path: 'test-results/overview-desktop.png', fullPage: true }); await tab(page, 'Dansers'); await page.getByRole('button', { name: 'Sam Testdanser verwijderen' }).click(); await confirmDelete(page); await expect(page.getByRole('cell', { name: 'Sam Testdanser', exact: true })).toHaveCount(0); await tab(page, 'Dansgroepen'); await expect(page.getByText(/0 dansers/)).toHaveCount(2); await page.getByRole('button', { name: 'Hiphop Selectie verwijderen' }).click(); await confirmDelete(page); await expect(page.getByRole('heading', { name: 'Hiphop Selectie', exact: true })).toHaveCount(0); await tab(page, 'Shows'); await page.getByRole('button', { name: 'Zomershow verwijderen' }).click(); await confirmDelete(page); await expect(page.getByText('Het podium wacht op je')).toBeVisible(); await tab(page, 'Overzicht'); await page.getByRole('button', { name: 'School verwijderen', exact: true }).click(); await confirmDelete(page); await expect(page).toHaveURL(/\/schools$/); await expect(page.getByRole('link', { name: new RegExp(schoolName) })).toHaveCount(0); expect(browserErrors).toEqual([]); }); test('links a coach and finalizes performances spread across multiple dates', async ({ page, request, }) => { const schoolResponse = await request.post(`${backend}/api/dance-schools`, { data: { name: `Coach show ${Date.now()}` }, }); const school = (await schoolResponse.json()) as School; schoolToDelete = school.id; const base = `${backend}/api/dance-schools/${school.id}`; const dancerResponse = await request.post(`${base}/dancers`, { data: { name: 'Dancing coach', birthDate: '1990-01-01' }, }); const dancer = (await dancerResponse.json()) as { id: number }; const groupIds: number[] = []; for (const name of ['Preschool', 'Adults']) { const response = await request.post(`${base}/dance-groups`, { data: { category: name, type: name === 'Preschool' ? 'PRE_SCHOOLER' : 'REGULAR', startAge: 0, endAge: 99, styles: [], }, }); const group = (await response.json()) as { id: number }; groupIds.push(group.id); await request.put(`${base}/dance-groups/${group.id}/dancers/${dancer.id}`); } const showResponse = await request.post(`${base}/shows`, { data: { name: 'Coach across dates', preferredDayParts: 1, dayParts: [ { date: '2027-06-01', type: 'AFTERNOON' }, { date: '2027-06-02', type: 'AFTERNOON' }, ], }, }); const show = (await showResponse.json()) as { id: number }; await request.put(`${base}/shows/${show.id}/assignments`, { data: { assignments: groupIds.map((id) => [id]) }, }); await page.goto(`/schools/${school.id}/shows`); await expect(page.getByText(/exceeds preferredDayParts=1/)).toBeVisible(); await tab(page, 'Dansers'); await page.getByText('Coaches koppelen aan dansers', { exact: true }).click(); await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' }); await expect(page.getByRole('combobox', { name: 'Dancing coach', exact: true })).toBeVisible(); await tab(page, 'Shows'); await expect(page.getByText('Planning voldoet aan alle harde regels.')).toBeVisible(); await page.getByRole('button', { name: 'Indeling en volgorde bewerken' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Definitief maken' }).click(); await expect( page.getByRole('dialog').getByRole('button', { name: 'Definitief maken' }), ).toBeDisabled(); const finalResponse = await request.get(`${base}/shows/${show.id}`); expect(((await finalResponse.json()) as { finalized: boolean }).finalized).toBe(true); }); test('shows a useful connection error and recovers on retry', async ({ page }) => { await page.route('**/api/dance-schools', (route) => route.abort('connectionfailed')); await page.goto('/schools'); await expect(page.getByRole('alert')).toContainText('niet bereikbaar'); await page.unroute('**/api/dance-schools'); await page.getByRole('button', { name: 'Opnieuw proberen' }).click(); await expect(page.getByRole('heading', { name: /Mijn dansscholen/ })).toBeVisible(); });