Visual-only mic button for the top strip per docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2. Renders three specified states (idle / listening / speaking) but Phase 8 only wires the idle state functionally. Phase 14 will toggle the state prop from the voice pipeline without changing this component's signature. Uses text-primary/bg-primary for volt (already migrated in phases 1-3) and literal #166534 / #a0a0a0 for forest and silver, which MIGRATION-PLAN.md §3 proposes as new semantic tokens that have not yet shipped. Part of Phase 8 of the Nexus layout overhaul (task 4 of 7). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
58 lines
1.7 KiB
TypeScript
58 lines
1.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { GlobalMicButton } from "./GlobalMicButton";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("GlobalMicButton", () => {
|
|
let container: HTMLDivElement;
|
|
let root: ReturnType<typeof createRoot> | 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(state: "idle" | "listening" | "speaking" = "idle") {
|
|
root = createRoot(container);
|
|
act(() => {
|
|
root!.render(<GlobalMicButton state={state} />);
|
|
});
|
|
return {
|
|
getButton: () => container.querySelector("button[aria-label='Voice']") as HTMLButtonElement,
|
|
getDataState: () => container.querySelector("button[aria-label='Voice']")?.getAttribute("data-state"),
|
|
};
|
|
}
|
|
|
|
it("renders a button with aria-label 'Voice' by default", () => {
|
|
const { getButton } = render();
|
|
expect(getButton()).not.toBeNull();
|
|
});
|
|
|
|
it("reflects the state prop via data-state", () => {
|
|
expect(render("idle").getDataState()).toBe("idle");
|
|
expect(render("listening").getDataState()).toBe("listening");
|
|
expect(render("speaking").getDataState()).toBe("speaking");
|
|
});
|
|
|
|
it("is a no-op on click in Phase 8 (does not throw)", () => {
|
|
const { getButton } = render();
|
|
act(() => {
|
|
getButton().click();
|
|
});
|
|
expect(getButton()).not.toBeNull();
|
|
});
|
|
});
|