diff --git a/ui/src/components/frame/TopStrip.test.tsx b/ui/src/components/frame/TopStrip.test.tsx new file mode 100644 index 00000000..3c6d06dd --- /dev/null +++ b/ui/src/components/frame/TopStrip.test.tsx @@ -0,0 +1,76 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { MemoryRouter } from "@/lib/router"; +import { TopStrip } from "./TopStrip"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +// Same stub as IconRail / ModeBreadcrumb tests — @/lib/router's Link +// (and useLocation consumers) trigger CompanyContext resolution. +vi.mock("@/context/CompanyContext", () => ({ + useCompany: () => ({ + companies: [], + selectedCompany: null, + selectedCompanyId: null, + setSelectedCompanyId: () => {}, + selectionSource: null, + loading: false, + }), +})); + +describe("TopStrip", () => { + let container: HTMLDivElement; + let root: ReturnType | null = null; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = null; + }); + + afterEach(() => { + if (root) { + act(() => { root!.unmount(); }); + root = null; + } + if (container.parentNode) container.remove(); + }); + + function render(pathname: string) { + root = createRoot(container); + act(() => { + root!.render( + + + + ); + }); + } + + it("renders the ModeBreadcrumb with derived segments", () => { + render("/NEX/assistant"); + const segment = container.querySelector("[data-testid='mode-breadcrumb-segment']"); + expect(segment?.textContent?.trim()).toBe("ASSISTANT"); + }); + + it("renders the CmdK button", () => { + render("/NEX/assistant"); + expect(container.querySelector("button[aria-label='Open command palette']")).not.toBeNull(); + }); + + it("renders the global mic button", () => { + render("/NEX/assistant"); + expect(container.querySelector("button[aria-label='Voice']")).not.toBeNull(); + }); + + it("is wrapped in a header element for landmark semantics", () => { + render("/NEX/assistant"); + const header = container.querySelector("header"); + expect(header).not.toBeNull(); + expect(header?.getAttribute("aria-label")).toBe("Top bar"); + }); +}); diff --git a/ui/src/components/frame/TopStrip.tsx b/ui/src/components/frame/TopStrip.tsx new file mode 100644 index 00000000..3ebe5193 --- /dev/null +++ b/ui/src/components/frame/TopStrip.tsx @@ -0,0 +1,26 @@ +import { CmdKButton } from "./CmdKButton"; +import { GlobalMicButton } from "./GlobalMicButton"; +import { ModeBreadcrumb } from "./ModeBreadcrumb"; + +/** + * TopStrip — the 48px top bar composed into the global frame in Layout.tsx + * (Task 6). Renders the mode breadcrumb on the left and the ⌘K + global + * mic buttons on the right. Charcoal bottom border, pure black background, + * sticky at the top of the main column. + * + * Per docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2. + */ +export function TopStrip() { + return ( +
+ +
+ + +
+
+ ); +}