All checks were successful
Build, test and publish / verify (push) Successful in 7m31s
332 lines
15 KiB
TypeScript
332 lines
15 KiB
TypeScript
import { test, expect, Page, APIRequestContext } 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}`, {
|
|
headers: await revision(request, schoolToDelete),
|
|
});
|
|
schoolToDelete = undefined;
|
|
}
|
|
});
|
|
|
|
async function revision(
|
|
request: APIRequestContext,
|
|
schoolId: number,
|
|
): Promise<Record<string, string>> {
|
|
const school = (await (
|
|
await request.get(`${backend}/api/dance-schools/${schoolId}`)
|
|
).json()) as School;
|
|
return { 'X-School-Revision': String(school.revision) };
|
|
}
|
|
|
|
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`,
|
|
{
|
|
headers: await revision(request, schoolToDelete),
|
|
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`,
|
|
{
|
|
headers: await revision(request, schoolToDelete),
|
|
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}`,
|
|
{ headers: await revision(request, schoolToDelete) },
|
|
);
|
|
}
|
|
await page.reload();
|
|
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`, {
|
|
headers: await revision(request, school.id),
|
|
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`, {
|
|
headers: await revision(request, school.id),
|
|
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}`, {
|
|
headers: await revision(request, school.id),
|
|
});
|
|
}
|
|
const showResponse = await request.post(`${base}/shows`, {
|
|
headers: await revision(request, school.id),
|
|
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`, {
|
|
headers: await revision(request, school.id),
|
|
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();
|
|
});
|
|
|
|
test('rejects a stale editor in another screen and allows reload and retry', async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const response = await request.post(`${backend}/api/dance-schools`, {
|
|
data: { name: `Concurrent school ${Date.now()}` },
|
|
});
|
|
const school = (await response.json()) as School;
|
|
schoolToDelete = school.id;
|
|
const other = await page.context().newPage();
|
|
try {
|
|
for (const screen of [page, other]) {
|
|
await screen.goto(`/schools/${school.id}/overview`);
|
|
await screen.getByRole('button', { name: 'School bewerken' }).click();
|
|
}
|
|
await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('First confirmed edit');
|
|
await other.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('Stale edit');
|
|
await save(page);
|
|
await other.getByRole('dialog').getByRole('button', { name: 'Opslaan', exact: true }).click();
|
|
await expect(other.getByRole('dialog').getByRole('alert')).toContainText('Herlaad');
|
|
await expect(other.getByRole('dialog').getByLabel('Naam', { exact: true })).toHaveValue(
|
|
'Stale edit',
|
|
);
|
|
await other.reload();
|
|
await expect(
|
|
other.getByRole('heading', { name: 'First confirmed edit', exact: true }),
|
|
).toBeVisible();
|
|
await other.getByRole('button', { name: 'School bewerken' }).click();
|
|
await other.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('Reloaded edit');
|
|
await save(other);
|
|
await expect(other.getByRole('heading', { name: 'Reloaded edit', exact: true })).toBeVisible();
|
|
} finally {
|
|
await other.close();
|
|
}
|
|
});
|