M44: harden release integrity and assurance
MobilityOps acceptance / backend (push) Failing after 20s
MobilityOps acceptance / frontend (push) Successful in 26s
MobilityOps acceptance / e2e (push) Skipped

This commit is contained in:
NuklearRabbit
2026-08-21 18:32:02 +02:00
parent 9e4fca5708
commit acd8b82b09
55 changed files with 1081 additions and 335 deletions
+6
View File
@@ -11,6 +11,12 @@ ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run build
FROM nginx:1.27-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
ARG VCS_REF=development
ARG BUILD_DATE=unknown
LABEL org.opencontainers.image.title="Fleet Ops Web" \
org.opencontainers.image.revision="$VCS_REF" \
org.opencontainers.image.created="$BUILD_DATE" \
org.opencontainers.image.source="https://fleetops.itworx.tech"
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
+52
View File
@@ -0,0 +1,52 @@
import { expect, test } from "@playwright/test";
test.describe.configure({ mode: "serial" });
test("public health and HTTPS-facing shell are available", async ({ page, request, baseURL }) => {
const readiness = await request.get("/health/ready");
expect(readiness.ok()).toBeTruthy();
expect(await readiness.json()).toMatchObject({ status: "ready", database: "up" });
await page.goto("/login");
await expect(page).toHaveTitle(/Fleet Ops/);
await expect(page.getByRole("button", { name: "Verken als Operationsmanager" })).toBeVisible();
if (baseURL?.startsWith("https://")) {
expect(page.url()).toMatch(/^https:\/\//);
}
});
test("non-destructive operator canary covers routes and grounded knowledge", async ({ page }) => {
const pageErrors: string[] = [];
const serverErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
page.on("response", (response) => {
if (response.status() >= 500) serverErrors.push(`${response.status()} ${response.url()}`);
});
await page.goto("/login");
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
for (const [path, heading] of [
["/vehicles", "Wagenpark"],
["/bookings", "Boekingen"],
["/data-quality", "Datakwaliteit"],
["/automation", "Integratiebeheer"],
["/audit", "Auditgeschiedenis"],
] as const) {
await page.goto(path);
await expect(page.getByRole("heading", { name: heading }).first()).toBeVisible();
}
await page.goto("/knowledge");
await page
.getByRole("textbox", { name: "Vraag" })
.fill("Wat moet ik doen wanneer een voertuig terugkomt met schade?");
await page.getByRole("button", { name: "Vraag stellen" }).click();
await expect(page.getByText("Onderbouwd met geciteerde procedures")).toBeVisible({
timeout: 20_000,
});
expect(pageErrors).toEqual([]);
expect(serverErrors).toEqual([]);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+52
View File
@@ -0,0 +1,52 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { expect, test } from "@playwright/test";
test.use({ bypassCSP: true, reducedMotion: "reduce" });
const require = createRequire(import.meta.url);
const axeSource = readFileSync(require.resolve("axe-core/axe.min.js"), "utf8");
type AxeViolation = {
id: string;
impact: string | null;
help: string;
nodes: Array<{ target: string[] }>;
};
async function seriousViolations(page: import("@playwright/test").Page): Promise<AxeViolation[]> {
await page.addStyleTag({
content: "*,*::before,*::after{animation:none!important;transition:none!important}",
});
await page.addScriptTag({ content: axeSource });
return page.evaluate(async () => {
const axe = (window as Window & {
axe: {
run: (
context: Document,
options: object,
) => Promise<{ violations: AxeViolation[] }>;
};
}).axe;
const result = await axe.run(document, {
runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21aa"] },
});
return result.violations.filter(
(violation) => violation.impact === "critical" || violation.impact === "serious",
);
});
}
test("principal routes have no serious automated accessibility violations", async ({ page }) => {
await page.goto("/login");
expect(await seriousViolations(page)).toEqual([]);
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
for (const route of ["/dashboard", "/data-quality", "/knowledge", "/automation", "/audit"] as const) {
await page.goto(route);
const violations = await seriousViolations(page);
expect(violations, `${route}: ${JSON.stringify(violations)}`).toEqual([]);
}
});
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
test("public entry and engineering highlights retain their visual hierarchy", async ({ page }) => {
await page.goto("/login");
await expect(page.locator(".login-access")).toHaveScreenshot("login-access.png", {
animations: "disabled",
maxDiffPixelRatio: 0.02,
});
await page.goto("/highlights");
await expect(page.locator("main")).toHaveScreenshot("highlights-main.png", {
animations: "disabled",
maxDiffPixelRatio: 0.02,
});
});
+1
View File
@@ -20,6 +20,7 @@
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "6.0.5",
"axe-core": "4.13.0",
"eslint": "9.39.5",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "7.1.1",
+2
View File
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "tsc -b && vite build",
"budget": "node ../scripts/check-frontend-budget.mjs",
"preview": "vite preview --host 0.0.0.0",
"lint": "tsc -b --noEmit && eslint .",
"test:e2e": "playwright test"
@@ -23,6 +24,7 @@
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "6.0.5",
"axe-core": "4.13.0",
"eslint": "9.39.5",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "7.1.1",
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
timeout: 30_000,
fullyParallel: false,
workers: 1,
snapshotPathTemplate: "{testDir}/__screenshots__/{arg}{ext}",
reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report" }]],
use: {
baseURL: process.env.MOBILITYOPS_PUBLIC_URL ?? "http://localhost:1228",
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e-live",
timeout: 30_000,
fullyParallel: false,
workers: 1,
reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-live-report" }]],
use: {
baseURL: process.env.MOBILITYOPS_PUBLIC_URL ?? "http://localhost:1228",
trace: "retain-on-failure",
screenshot: "only-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
],
});
+3 -3
View File
@@ -5,7 +5,7 @@
font-synthesis: none;
--ink: #0f172a;
--ink-soft: #344256;
--muted: #657386;
--muted: #5d6a7c;
--muted-light: #94a0af;
--canvas: #f5f7fa;
--surface: #ffffff;
@@ -140,7 +140,7 @@ a:hover { color: var(--teal); }
.demo-badge-close { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; }
.demo-badge-close svg { width: 14px; height: 14px; }
#main-content { width: min(1320px, calc(100% - 56px)); margin: 0 auto; padding: 38px 0 64px; flex: 1; }
.app-footer { min-height: 52px; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 0 28px; color: var(--muted); border-top: 1px solid var(--line); font-size: .68rem; }
.app-footer { min-height: 52px; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 0 28px; color: #5f6d80; border-top: 1px solid var(--line); font-size: .68rem; }
.mobile-nav, .nav-scrim { display: none; }
.page { animation: page-in .28s ease both; }
@@ -231,7 +231,7 @@ a:hover { color: var(--teal); }
.integration-list li:last-child, .recent-list li:last-child { border-bottom: 0; }.integration-list li > div, .recent-list li > div { flex: 1; display: grid; gap: 3px; }
.integration-list strong, .recent-list strong { font-size: .73rem; }.integration-list div span, .recent-list div span { color: var(--muted); font-size: .65rem; }
.integration-mark { width: 31px; height: 31px; display: grid; place-items: center; border-radius: var(--radius); color: white; font-size: .58rem; font-weight: 800; letter-spacing: -.04em; }
.integration-n8n { background: #e85d36; }.integration-rag { background: #4a4b95; }.integration-mcp { background: #314358; }
.integration-n8n { background: #b84424; }.integration-rag { background: #4a4b95; }.integration-mcp { background: #314358; }
.activity-icon { width: 30px; height: 30px; display: grid; place-items: center; color: var(--teal-dark); background: var(--teal-pale); border-radius: 50%; }.activity-icon svg { width: 15px; }
.recent-list time { color: var(--muted); font-size: .62rem; white-space: nowrap; }