test: add the end-to-end suite, on desktop and on a phone
Twenty-six tests across two viewports, covering what the spec asks for: changing today's hours reaches the panel, a message survives the translation service being unavailable, a closure period closes the days it covers — plus the authorisation paths and the sign-out regression. They run against the standalone build served the way the container serves it, not `next start`, which refuses to work with standalone output anyway. The suite therefore exercises the artifact that ships rather than a second arrangement that could drift from it. Sign-in mints the session cookie Auth.js would have issued rather than driving Authentik. What is under test is the application's behaviour for a given role; the handshake itself is verified against the live provider separately, and standing up an identity provider per run would trade a lot of machinery for coverage of somebody else's code. The secret lives in one module imported by both the config and the fixtures — when it differed, every signed-in test failed at once while looking like an authorisation bug. Database access goes through plain SQL rather than the Prisma client, whose generated module format Playwright's loader and Next's bundler disagree about. That traded one problem for a subtler one: node-postgres parses a DATE column into a local-midnight Date, so reading it back shifted the day at UTC+2. Dates are read as text now. The mobile profile runs on Chromium: WebKit needs system packages only root can install, and a suite nobody can run locally is a suite nobody runs. The config says how to switch to the real engine. Two real defects surfaced, both found by the tests rather than by reading. The seven "Ouvert" checkboxes on the hours page were indistinguishable to a screen reader; each now names its day. And on a phone the signed-in address appeared nowhere at all — the header hides it to save room — so nobody could tell which account was about to sign an audit entry on a device the shop shares. It is on the dashboard now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { expect, pairDevice, query, resetDatabase, signIn, test, today } from './fixtures';
|
||||
|
||||
async function panelFilename(request: import('@playwright/test').APIRequestContext, token: string) {
|
||||
const response = await request.get('/api/display', { headers: { 'Access-Token': token } });
|
||||
expect(response.ok()).toBeTruthy();
|
||||
return ((await response.json()) as { filename: string }).filename;
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
||||
test('changing today’s hours reaches the panel', async ({ page, request }) => {
|
||||
const token = await pairDevice();
|
||||
const before = await panelFilename(request, token);
|
||||
|
||||
await signIn(page, 'admin');
|
||||
await page.goto('/admin/exceptions');
|
||||
|
||||
await page.getByRole('button', { name: /Fermé aujourd’hui/ }).click();
|
||||
await expect(page.getByText(/La boutique est fermée aujourd’hui/)).toBeVisible();
|
||||
|
||||
// Stored as a manual exception for today, in the shop's timezone.
|
||||
// Read as text: node-postgres parses a DATE column into a local-midnight
|
||||
// Date, so toISOString() shifts it back a day at UTC+2. The stored value is
|
||||
// right; the interpretation would not be.
|
||||
const [exception] = await query<{ date: string; isClosed: boolean }>(
|
||||
'SELECT date::text AS date, "isClosed" FROM schedule_exceptions',
|
||||
);
|
||||
expect(exception?.date).toBe(today());
|
||||
expect(exception?.isClosed).toBe(true);
|
||||
|
||||
// And the panel is handed a different image, so it will redraw.
|
||||
expect(await panelFilename(request, token)).not.toBe(before);
|
||||
|
||||
await page.goto('/admin');
|
||||
await expect(page.getByText(/aujourd’hui/).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('the reference week can be edited and shows what will change', async ({ page }) => {
|
||||
await signIn(page, 'admin');
|
||||
await page.goto('/admin/horaires');
|
||||
|
||||
// Monday is closed in the seeded week; open it.
|
||||
await page.getByRole('checkbox', { name: /Ouvert le lundi/i }).check();
|
||||
await expect(page.getByText(/Lundi sera modifié|Sera modifié|1 jour/i).first()).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
||||
await expect(page.getByText(/Horaires enregistrés|enregistrés/i)).toBeVisible();
|
||||
|
||||
const [monday] = await query<{ isClosed: boolean }>(
|
||||
'SELECT "isClosed" FROM weekly_schedules WHERE "dayOfWeek" = 1',
|
||||
);
|
||||
expect(monday?.isClosed).toBe(false);
|
||||
});
|
||||
|
||||
test('a closure period closes the days it covers', async ({ page }) => {
|
||||
const start = today();
|
||||
const end = today(3);
|
||||
|
||||
await signIn(page, 'admin');
|
||||
await page.goto('/admin/vacances');
|
||||
|
||||
await page.getByLabel('Du').fill(start);
|
||||
await page.getByLabel('Au (inclus)').fill(end);
|
||||
await page.getByLabel(/Libellé/).fill('Congés de test');
|
||||
await page.getByRole('button', { name: 'Ajouter' }).click();
|
||||
|
||||
await expect(page.getByText(/Période de fermeture enregistrée/)).toBeVisible();
|
||||
await expect(page.getByText('Congés de test')).toBeVisible();
|
||||
|
||||
// The resolver reads the period directly, so the dashboard shows the days
|
||||
// closed without anything having been written into the exception table.
|
||||
await page.goto('/admin');
|
||||
await expect(page.getByText('Congés de test').first()).toBeVisible();
|
||||
const [{ count }] = await query<{ count: string }>('SELECT count(*) FROM schedule_exceptions');
|
||||
expect(Number(count)).toBe(0);
|
||||
});
|
||||
|
||||
test('overlapping periods are refused by name', async ({ page }) => {
|
||||
const start = today();
|
||||
const end = today(5);
|
||||
|
||||
await signIn(page, 'admin');
|
||||
await page.goto('/admin/vacances');
|
||||
|
||||
await page.getByLabel('Du').fill(start);
|
||||
await page.getByLabel('Au (inclus)').fill(end);
|
||||
await page.getByLabel(/Libellé/).fill('Première');
|
||||
await page.getByRole('button', { name: 'Ajouter' }).click();
|
||||
await expect(page.getByText(/enregistrée/)).toBeVisible();
|
||||
// The form empties itself once the server answers; refilling before that
|
||||
// would have the reset wipe what was just typed.
|
||||
await expect(page.getByLabel(/Libellé/)).toHaveValue('');
|
||||
|
||||
await page.getByLabel('Du').fill(start);
|
||||
await page.getByLabel('Au (inclus)').fill(end);
|
||||
await page.getByLabel(/Libellé/).fill('Seconde');
|
||||
await page.getByRole('button', { name: 'Ajouter' }).click();
|
||||
|
||||
await expect(page.getByText(/chevauche « Première »/)).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user