M44: harden release integrity and assurance
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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([]);
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
Generated
+1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] } },
|
||||
],
|
||||
});
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user