The dashboard greeting was a fully static "Goedemorgen..." regardless of
actual time of day. New frontend/src/i18n/greeting.ts::getGreetingPeriod
is a pure, clock-injectable function resolving one of 4 periods (05:00-
11:59 morning, 12:00-17:59 afternoon, 18:00-22:59 evening, 23:00-04:59
night) against Europe/Brussels wall-clock time via
Intl.DateTimeFormat({ timeZone, hourCycle: "h23" }), which is DST-safe
by construction.
useGreetingPeriod.ts wires this into React with a 30s poll so the
greeting rolls over live while the app stays open, no reload required.
Each period now has its own greeting word and accompanying sentence in
all 3 languages (dashboard.json), replacing both the fixed "Goedemorgen"
and the fixed "Here's the fleet" follow-up sentence. Night never says
"Goedenacht" (used as a farewell, not a welcome, in Dutch).
108 lines
5.0 KiB
TypeScript
108 lines
5.0 KiB
TypeScript
import { expect, type Page, test } from "@playwright/test";
|
|
|
|
// Browser-context evidence for the time-dependent Europe/Brussels dashboard greeting
|
|
// (section 9 / 11 / 16 of the Fleet Ops final localization brief): the real rendered app,
|
|
// in all 3 languages, at every required boundary instant, using Playwright's clock API to
|
|
// control the browser's Date without waiting on real wall-clock time. Also proves the
|
|
// greeting updates live (no reload) when the period rolls over while the app stays open.
|
|
|
|
// 2026-01-15 is CET (UTC+1): Brussels hour = UTC hour + 1.
|
|
function cet(hour: number, minute = 0, second = 0): Date {
|
|
return new Date(Date.UTC(2026, 0, 15, hour - 1, minute, second));
|
|
}
|
|
|
|
const BOUNDARY_CASES: Array<{ time: Date; label: string; period: string }> = [
|
|
{ time: cet(4, 59), label: "04:59", period: "night" },
|
|
{ time: cet(5, 0), label: "05:00", period: "morning" },
|
|
{ time: cet(11, 59), label: "11:59", period: "morning" },
|
|
{ time: cet(12, 0), label: "12:00", period: "afternoon" },
|
|
{ time: cet(17, 59), label: "17:59", period: "afternoon" },
|
|
{ time: cet(18, 0), label: "18:00", period: "evening" },
|
|
{ time: cet(22, 59), label: "22:59", period: "evening" },
|
|
{ time: cet(23, 0), label: "23:00", period: "night" },
|
|
];
|
|
|
|
const EXPECTED_TITLE: Record<string, Record<string, string>> = {
|
|
"nl-BE": {
|
|
morning: "Goedemorgen. Hier is de status van je wagenpark voor vandaag.",
|
|
afternoon: "Goedemiddag. Hier is het actuele overzicht van je wagenpark.",
|
|
evening: "Goedenavond. Hier is het overzicht van je wagenpark voor vanavond.",
|
|
night: "Welkom terug. Hier is het laatste overzicht van je wagenpark.",
|
|
},
|
|
"en-GB": {
|
|
morning: "Good morning. Here's today's fleet status.",
|
|
afternoon: "Good afternoon. Here's the current overview of your fleet.",
|
|
evening: "Good evening. Here's this evening's fleet overview.",
|
|
night: "Welcome back. Here's the latest overview of your fleet.",
|
|
},
|
|
"fr-BE": {
|
|
morning: "Bonjour. Voici l'état de votre flotte pour aujourd'hui.",
|
|
afternoon: "Bonjour. Voici l'aperçu actuel de votre flotte.",
|
|
evening: "Bonsoir. Voici l'aperçu de votre flotte pour ce soir.",
|
|
night: "Bon retour. Voici le dernier aperçu de votre flotte.",
|
|
},
|
|
};
|
|
|
|
async function loginAsOperationsManager(page: Page) {
|
|
await page.goto("/login");
|
|
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
|
|
await expect(page).toHaveURL(/\/dashboard$/);
|
|
}
|
|
|
|
async function switchLanguage(page: Page, language: "nl-BE" | "en-GB" | "fr-BE") {
|
|
// At the default desktop viewport, only the topbar's compact switcher is visible --
|
|
// the sidebar's full switcher is `display: none` until the <960px breakpoint.
|
|
await page.locator(".language-switcher-compact select").selectOption(language);
|
|
}
|
|
|
|
const dashboardHeading = (page: Page) => page.locator(".page-header h1");
|
|
|
|
for (const language of ["nl-BE", "en-GB", "fr-BE"] as const) {
|
|
test(`dashboard greeting matches every required boundary time in ${language}`, async ({ page }) => {
|
|
await page.clock.install({ time: BOUNDARY_CASES[0].time });
|
|
await loginAsOperationsManager(page);
|
|
if (language !== "nl-BE") {
|
|
await switchLanguage(page, language);
|
|
}
|
|
|
|
for (const { time, label, period } of BOUNDARY_CASES) {
|
|
await page.clock.setFixedTime(time);
|
|
await page.reload();
|
|
await expect(dashboardHeading(page), `${language} @ ${label} Brussels time (expected period: ${period})`).toHaveText(
|
|
EXPECTED_TITLE[language][period],
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
test("dashboard greeting updates live across a period rollover without a page reload", async ({ page }) => {
|
|
// Start at 11:59:31 Brussels -- 29s before the 12:00 boundary.
|
|
await page.clock.install({ time: cet(11, 59, 31) });
|
|
await loginAsOperationsManager(page);
|
|
await expect(dashboardHeading(page)).toHaveText(EXPECTED_TITLE["nl-BE"].morning);
|
|
|
|
// Advance 30s of fake time (crossing 12:00) so the hook's 30s poll interval fires and
|
|
// recomputes the period -- no page.reload() call anywhere in this test.
|
|
await page.clock.fastForward(30_000);
|
|
await expect(dashboardHeading(page)).toHaveText(EXPECTED_TITLE["nl-BE"].afternoon);
|
|
});
|
|
|
|
test("dashboard greeting updates immediately on language switch without changing the time period", async ({ page }) => {
|
|
await page.clock.install({ time: cet(9, 0) });
|
|
await loginAsOperationsManager(page);
|
|
await expect(dashboardHeading(page)).toHaveText(EXPECTED_TITLE["nl-BE"].morning);
|
|
|
|
await switchLanguage(page, "en-GB");
|
|
await expect(dashboardHeading(page)).toHaveText(EXPECTED_TITLE["en-GB"].morning);
|
|
|
|
await switchLanguage(page, "fr-BE");
|
|
await expect(dashboardHeading(page)).toHaveText(EXPECTED_TITLE["fr-BE"].morning);
|
|
});
|
|
|
|
test("dashboard never shows 'Goedenacht' as a greeting at any hour", async ({ page }) => {
|
|
await page.clock.install({ time: cet(2, 0) });
|
|
await loginAsOperationsManager(page);
|
|
await expect(dashboardHeading(page)).not.toContainText("Goedenacht");
|
|
await expect(dashboardHeading(page)).toContainText("Welkom terug");
|
|
});
|