diff --git a/webui/conformance/ConformanceApp.tsx b/webui/conformance/ConformanceApp.tsx index a0056d2..57bed16 100644 --- a/webui/conformance/ConformanceApp.tsx +++ b/webui/conformance/ConformanceApp.tsx @@ -31,7 +31,7 @@ import { createQuickAccessLaunchContext, quickAccessLaunchState } from "../src/platform/launchContext"; -import type { ApiSettings, AuthInfo, QuickAccessToolMetadata } from "../src/types"; +import type { ApiSettings, AuthInfo, EffectiveViewProjection, QuickAccessToolMetadata } from "../src/types"; export default function ConformanceApp() { const location = useLocation(); @@ -172,6 +172,20 @@ export default function ConformanceApp() { function QuickAccessScenario() { const location = useLocation(); + const mode = new URLSearchParams(location.search).get("quick-access"); + const viewContext = useMemo( + () => mode === "focused" || mode === "stale-focus" + ? { + ...CONFORMANCE_VIEW_PROJECTION, + presentation: { + quickAccessFocusedToolIds: mode === "focused" + ? ["files.recent"] + : ["mail.compose"] + } + } + : null, + [mode] + ); const launchContext = useMemo(() => createQuickAccessLaunchContext({ pathname: location.pathname, search: location.search, @@ -185,8 +199,9 @@ function QuickAccessScenario() { tenantId: "tenant-1", label: "RPP-2026-0001 ยท Anwohnerparkausweis" }, - temporalContext: { validityMode: "current", validAt: null, recordedAt: null } - }), [location.hash, location.pathname, location.search]); + temporalContext: { validityMode: "current", validAt: null, recordedAt: null }, + viewContext + }), [location.hash, location.pathname, location.search, viewContext]); return { await route.fulfill({ contentType: "application/json", - body: JSON.stringify({ - categories: [{ - id: "files", - label: "Files", - description: "Files beside the current task", - icon: "files", - order: 10, - enabled: true, - forced: false, - locked_by: null, - tools: [{ - contract_version: "1", - id: "files.recent", - module_id: "files", - category_id: "files", - label: "Recent files", - description: "Select an authorized file without leaving this case.", - icon: "files", - surface_id: "files.route.files", - full_page_path: "/files", - required_all: [], - required_any: [], - order: 10, - default_enabled: true, - modes: ["select"], - availability: "global", - accepted_reference_kinds: [], - returned_reference_kinds: ["files.file-version"], - help_context_id: "files.quick_access.files", - enabled: true, - forced: false, - locked_by: null - }] - }], - diagnostics: [] - }) + body: JSON.stringify(quickAccessPayload(false)) }); }); await page.setViewportSize({ width: 390, height: 844 }); @@ -145,6 +110,52 @@ test("Quick Access preserves focus and adapts to a narrow viewport", async ({ pa await expect(filesTrigger).toBeFocused(); }); +test("View focus has a deliberate permission-derived all-tools escape", async ({ page }) => { + await page.route("**/api/v1/quick-access/effective*", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify(quickAccessPayload(true)) + }); + }); + await page.goto("/?theme=light&quick-access=focused"); + + const rail = page.locator(".quick-access-rail"); + await expect(rail).toHaveAttribute("data-view-focus-mode", "focused"); + await expect(page.getByRole("button", { name: "Files" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Messages" })).toHaveCount(0); + + const allTools = page.locator(".quick-access-view-mode-button"); + await expect(allTools).toHaveAttribute("aria-pressed", "false"); + await allTools.click(); + await expect(rail).toHaveAttribute("data-view-focus-mode", "all"); + await expect(page.getByRole("button", { name: "Messages" })).toBeVisible(); + + await page.getByRole("button", { name: "Messages" }).click(); + const explanation = page.locator(".quick-access-view-mode[role='status']"); + await expect(explanation).toHaveAttribute("data-view-focus-mode", "all"); + await explanation.getByRole("button").click(); + await expect(rail).toHaveAttribute("data-view-focus-mode", "focused"); + await expect(page.getByRole("button", { name: "Messages" })).toHaveCount(0); +}); + +test("a stale View focus falls back safely in a sparse optional-module catalogue", async ({ page }) => { + await page.route("**/api/v1/quick-access/effective*", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify(quickAccessPayload(false)) + }); + }); + await page.goto("/?theme=light&quick-access=stale-focus"); + + const rail = page.locator(".quick-access-rail"); + await expect(rail).toHaveAttribute("data-view-focus-mode", "unavailable"); + await expect(page.getByRole("button", { name: "Files" })).toBeVisible(); + await expect(page.locator(".quick-access-view-mode-button")).toHaveCount(0); + await page.getByRole("button", { name: "Files" }).click(); + await expect(page.locator(".quick-access-view-mode[role='status']")) + .toHaveAttribute("data-view-focus-mode", "unavailable"); +}); + test("light and dark desktop geometry remains stable", async ({ page }) => { await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto("/?theme=light"); @@ -179,3 +190,76 @@ test("narrow layout preserves task order without horizontal overflow", async ({ expect(Math.abs(actionBarRightEdge - saveRightEdge)).toBeLessThanOrEqual(1); await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-light-narrow.png", { animations: "disabled", maxDiffPixelRatio: 0.005 }); }); + +function quickAccessPayload(includeMessages: boolean) { + const files = { + id: "files", + label: "Files", + description: "Files beside the current task", + icon: "files", + order: 10, + enabled: true, + forced: false, + locked_by: null, + tools: [{ + contract_version: "1", + id: "files.recent", + module_id: "files", + category_id: "files", + label: "Recent files", + description: "Select an authorized file without leaving this case.", + icon: "files", + surface_id: "files.route.files", + full_page_path: "/files", + required_all: [], + required_any: [], + order: 10, + default_enabled: true, + modes: ["select"], + availability: "global", + accepted_reference_kinds: [], + returned_reference_kinds: ["files.file-version"], + help_context_id: "files.quick_access.files", + enabled: true, + forced: false, + locked_by: null + }] + }; + const messages = { + id: "messages", + label: "Messages", + description: "Messages beside the current task", + icon: "messages", + order: 20, + enabled: true, + forced: false, + locked_by: null, + tools: [{ + contract_version: "1", + id: "postbox.unread", + module_id: "postbox", + category_id: "messages", + label: "Unread messages", + description: "Open an authorized unread message.", + icon: "messages", + surface_id: "postbox.route.postbox", + full_page_path: "/postbox", + required_all: [], + required_any: [], + order: 10, + default_enabled: true, + modes: ["view"], + availability: "global", + accepted_reference_kinds: [], + returned_reference_kinds: ["postbox.message"], + help_context_id: "postbox.quick_access.messages", + enabled: true, + forced: false, + locked_by: null + }] + }; + return { + categories: includeMessages ? [files, messages] : [files], + diagnostics: [] + }; +}