"Opens later at" and "closes earlier at" were prefilled with 14:00 and 16:00 — arbitrary values, wrong on most days, which left the preview showing hours nobody had asked for until both fields were corrected. They now start from the day's real opening and closing, so any adjustment is relative to what that day actually is. A day with a lunch break is bounded by its first opening and its last closing, which is what these two actions act on. Changing the date changes the hours the times are relative to, so a value typed for Tuesday must not survive into Thursday. The fields live in a component keyed on the date and React resets them by remounting — the React compiler rightly refuses an effect that writes state back, and it is the clearer arrangement anyway. The end-to-end test picks a trading day rather than assuming today is one. The first version guarded the assertions behind a visibility check, which would have passed in silence on exactly the days the controls matter least. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
152 lines
6.0 KiB
TypeScript
152 lines
6.0 KiB
TypeScript
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;
|
||
}
|
||
|
||
/** The seeded week trades Tuesday to Saturday; Sunday and Monday are closed. */
|
||
function nextTradingDay(): string {
|
||
for (let offset = 0; offset < 7; offset += 1) {
|
||
const date = today(offset);
|
||
const weekday = new Date(`${date}T00:00:00.000Z`).getUTCDay();
|
||
if (weekday >= 2) {
|
||
return date;
|
||
}
|
||
}
|
||
throw new Error('aucun jour ouvré dans les sept prochains jours');
|
||
}
|
||
|
||
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.getByRole('status')).toContainText(/sera fermée aujourd’hui/);
|
||
|
||
// 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('a quick change can target a day other than today', async ({ page }) => {
|
||
const target = today(3);
|
||
|
||
await signIn(page, 'admin');
|
||
await page.goto('/admin/exceptions');
|
||
|
||
// The date defaults to today; closing next Thursday must not cost more
|
||
// clicks than closing this afternoon.
|
||
await page.getByLabel('Jour à modifier').selectOption(target);
|
||
await page.getByRole('button', { name: /^Fermé le / }).click();
|
||
|
||
await expect(page.getByRole('status')).toContainText(/sera fermée/);
|
||
|
||
const [exception] = await query<{ date: string; isClosed: boolean }>(
|
||
'SELECT date::text AS date, "isClosed" FROM schedule_exceptions',
|
||
);
|
||
expect(exception?.date).toBe(target);
|
||
expect(exception?.isClosed).toBe(true);
|
||
});
|
||
|
||
test('the quick controls start from the selected day’s own hours', async ({ page }) => {
|
||
// The seeded week is closed on Sunday and Monday, so the day is chosen
|
||
// rather than assumed: a conditional assertion would pass in silence on the
|
||
// days it matters least.
|
||
const tradingDay = nextTradingDay();
|
||
|
||
await signIn(page, 'admin');
|
||
await page.goto('/admin/exceptions');
|
||
await page.getByLabel('Jour à modifier').selectOption(tradingDay);
|
||
|
||
// An arbitrary 14:00 would be wrong on most days and leave the preview
|
||
// showing hours nobody asked for.
|
||
await expect(page.getByLabel('Nouvelle heure d’ouverture')).toHaveValue('10:00');
|
||
await expect(page.getByLabel('Nouvelle heure de fermeture')).toHaveValue('18:30');
|
||
await expect(page.getByText('Habituellement 10:00 – 18:30.')).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();
|
||
});
|