Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/shell/acceptance/keyboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// @vitest-environment node
import { chromium, expect as browserExpect, type Browser } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { afterAll, beforeAll, expect, it } from "vitest";

let server: ViteDevServer | undefined;
let browser: Browser | undefined;
let url: string;

// The server and browser belong to this foreground test run and are closed even on failure.
beforeAll(async () => {
server = await createServer({
define: { "import.meta.env.VITE_TAURI_MOCKS": JSON.stringify("true") },
server: { host: "127.0.0.1", port: 0, strictPort: false, hmr: false, watch: null },
});
await server.listen();
url = server.resolvedUrls!.local[0];
browser = await chromium.launch();
}, 30_000);

afterAll(async () => {
try { await browser?.close(); } finally { await server?.close(); }
});

async function checkTab(path: string) {
const page = await browser!.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.addInitScript(() => {
localStorage.setItem("projecta.settings.featureFlag.d1_neue_oberflaeche", "1");
localStorage.setItem("projecta.settings.featureFlag.weitere_bereiche", "0");
});
await page.goto(`${url}#${path}`);
const shell = page.locator(".g-shell");
await browserExpect(shell).toBeVisible();
await browserExpect(page.locator(".g-shell-content")).not.toBeEmpty();
expect(new URL(page.url()).hash).toBe(`#${path}`);
const navigation = shell.getByRole("navigation", { name: "Hauptnavigation" });
const first = navigation.getByRole("link", { name: "Leitstand", exact: true });
const second = navigation.getByRole("link", { name: "Einstellungen", exact: true });
await page.keyboard.press("Tab");
await browserExpect(first).toBeFocused();
await browserExpect(first).toBeVisible();
await page.keyboard.press("Tab");
await browserExpect(second).toBeFocused();
await browserExpect(second).toBeVisible();
await page.keyboard.press("Shift+Tab");
await browserExpect(first).toBeFocused();
} finally {
await page.close();
}
}

it("Tab enters the shell and Shift-Tab returns at /leitstand", () => checkTab("/leitstand"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /leitstand/verlauf", () => checkTab("/leitstand/verlauf"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /leitstand/betrieb", () => checkTab("/leitstand/betrieb"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /leitstand/klassisch", () => checkTab("/leitstand/klassisch"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /leitstand/werkzeuge", () => checkTab("/leitstand/werkzeuge"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /leitstand/terminal", () => checkTab("/leitstand/terminal"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /einstellungen", () => checkTab("/einstellungen"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /einstellungen/mcp", () => checkTab("/einstellungen/mcp"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /einstellungen/benachrichtigungen", () => checkTab("/einstellungen/benachrichtigungen"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /einstellungen/projekt", () => checkTab("/einstellungen/projekt"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /einstellungen/klassisch", () => checkTab("/einstellungen/klassisch"), 15_000);
it("Tab enters the shell and Shift-Tab returns at /start", () => checkTab("/start"), 15_000);
15 changes: 15 additions & 0 deletions src/shell/acceptance/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Registry entries plus links exposed by the mounted Leitstand and settings screens.
export const ACCEPTANCE_PATHS = [
"/leitstand",
"/leitstand/verlauf",
"/leitstand/betrieb",
"/leitstand/klassisch",
"/leitstand/werkzeuge",
"/leitstand/terminal",
"/einstellungen",
"/einstellungen/mcp",
"/einstellungen/benachrichtigungen",
"/einstellungen/projekt",
"/einstellungen/klassisch",
"/start",
] as const;
48 changes: 48 additions & 0 deletions src/shell/acceptance/stopband.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { act, cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { isFeatureFlagEnabled } from "../../flags/featureFlags";
import { Shell } from "../Shell";
import { visibleRoutes } from "../routes";
import { ACCEPTANCE_PATHS } from "./routes";

vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn(async (command: string) => command === "get_emergency_stop" ? true : []),
}));
vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn(async () => () => {}) }));

beforeEach(() => { localStorage.clear(); });
afterEach(cleanup);

it("covers every scope registry path with D1 on by default", () => {
expect(isFeatureFlagEnabled("d1_neue_oberflaeche")).toBe(true);
expect(isFeatureFlagEnabled("weitere_bereiche")).toBe(false);
expect(visibleRoutes().map((route) => route.id)).toEqual(["leitstand", "einstellungen", "start"]);
for (const route of visibleRoutes()) {
for (const path of [route.path, ...route.tabs.map((tab) => tab.path)]) {
expect(ACCEPTANCE_PATHS).toContain(path);
}
}
});

async function checkStopBand(path: string) {
window.location.hash = `#${path}`;
await act(async () => { render(<Shell />); });
const band = screen.getByText("Not-Aus ist aktiv: neue Aufgaben sind gesperrt.").closest('[role="status"]');
expect(band).not.toBeNull();
expect(within(band as HTMLElement).getByRole("button", { name: "Fortsetzen" })).toBeVisible();
expect(screen.getByTestId("emergency-stop")).toBeVisible();
expect(window.location.hash).toBe(`#${path}`);
}

it("renders the active StopBand and stop control at /leitstand", () => checkStopBand("/leitstand"));
it("renders the active StopBand and stop control at /leitstand/verlauf", () => checkStopBand("/leitstand/verlauf"));
it("renders the active StopBand and stop control at /leitstand/betrieb", () => checkStopBand("/leitstand/betrieb"));
it("renders the active StopBand and stop control at /leitstand/klassisch", () => checkStopBand("/leitstand/klassisch"));
it("renders the active StopBand and stop control at /leitstand/werkzeuge", () => checkStopBand("/leitstand/werkzeuge"));
it("renders the active StopBand and stop control at /leitstand/terminal", () => checkStopBand("/leitstand/terminal"));
it("renders the active StopBand and stop control at /einstellungen", () => checkStopBand("/einstellungen"));
it("renders the active StopBand and stop control at /einstellungen/mcp", () => checkStopBand("/einstellungen/mcp"));
it("renders the active StopBand and stop control at /einstellungen/benachrichtigungen", () => checkStopBand("/einstellungen/benachrichtigungen"));
it("renders the active StopBand and stop control at /einstellungen/projekt", () => checkStopBand("/einstellungen/projekt"));
it("renders the active StopBand and stop control at /einstellungen/klassisch", () => checkStopBand("/einstellungen/klassisch"));
it("renders the active StopBand and stop control at /start", () => checkStopBand("/start"));