feat(bd-grs): implement app navigation with keyboard shortcuts

Add navigation with keyboard shortcuts (Cmd+1/2/3/4/,) for Focus, Queue, Inbox, Debug, and Settings views.

Components:
- useKeyboardShortcuts hook: handles global shortcuts with editable element detection
- Navigation component: standalone nav bar (not used, but available)
- SettingsView placeholder: Phase 5 stub
- AppShell: integrated keyboard shortcuts and Settings button

Tests:
- useKeyboardShortcuts: 11 tests covering shortcuts, modifiers, editable detection
- Navigation: 12 tests covering nav items, badges, click, keyboard shortcuts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 11:01:59 -05:00
parent 251ae44a56
commit 0efc09d4bd
6 changed files with 633 additions and 1 deletions

View File

@@ -90,7 +90,7 @@ describe("AppShell", () => {
});
renderWithProviders(<AppShell />);
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByTestId("queue-badge")).toHaveTextContent("3");
});
it("opens quick capture overlay on global shortcut event", async () => {

View File

@@ -0,0 +1,159 @@
/**
* Tests for Navigation component
*
* Tests navigation UI elements, active state, badge counts, and keyboard shortcuts.
*/
import { describe, it, expect, beforeEach } from "vitest";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Navigation } from "@/components/Navigation";
import { useNavStore } from "@/stores/nav-store";
import { useFocusStore } from "@/stores/focus-store";
import { useInboxStore } from "@/stores/inbox-store";
import { makeFocusItem } from "../helpers/fixtures";
describe("Navigation", () => {
beforeEach(() => {
useNavStore.setState({ activeView: "focus" });
useFocusStore.setState({
current: null,
queue: [],
isLoading: false,
error: null,
});
useInboxStore.setState({
items: [],
isLoading: false,
error: null,
});
});
it("renders nav items for all views", () => {
render(<Navigation />);
expect(screen.getByRole("button", { name: /focus/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /queue/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /inbox/i })).toBeInTheDocument();
});
it("highlights the active view", () => {
useNavStore.setState({ activeView: "queue" });
render(<Navigation />);
const queueButton = screen.getByRole("button", { name: /queue/i });
expect(queueButton).toHaveAttribute("data-active", "true");
const focusButton = screen.getByRole("button", { name: /focus/i });
expect(focusButton).toHaveAttribute("data-active", "false");
});
it("shows queue badge count when items exist", () => {
useFocusStore.setState({
current: makeFocusItem({ id: "a" }),
queue: [makeFocusItem({ id: "b" }), makeFocusItem({ id: "c" })],
});
render(<Navigation />);
const badge = screen.getByTestId("queue-badge");
expect(badge).toHaveTextContent("3");
});
it("does not show queue badge when no items", () => {
render(<Navigation />);
expect(screen.queryByTestId("queue-badge")).not.toBeInTheDocument();
});
it("navigates to queue view on click", async () => {
const user = userEvent.setup();
render(<Navigation />);
await user.click(screen.getByRole("button", { name: /queue/i }));
expect(useNavStore.getState().activeView).toBe("queue");
});
it("navigates to inbox view on click", async () => {
const user = userEvent.setup();
render(<Navigation />);
await user.click(screen.getByRole("button", { name: /inbox/i }));
expect(useNavStore.getState().activeView).toBe("inbox");
});
it("navigates to settings view on settings button click", async () => {
const user = userEvent.setup();
render(<Navigation />);
await user.click(screen.getByRole("button", { name: /settings/i }));
expect(useNavStore.getState().activeView).toBe("settings");
});
// Keyboard shortcut tests
function dispatchKeyEvent(
key: string,
opts: { metaKey?: boolean } = {}
): void {
const event = new KeyboardEvent("keydown", {
key,
metaKey: opts.metaKey ?? false,
bubbles: true,
});
document.dispatchEvent(event);
}
it("navigates to focus view on Cmd+1", () => {
useNavStore.setState({ activeView: "queue" });
render(<Navigation />);
act(() => {
dispatchKeyEvent("1", { metaKey: true });
});
expect(useNavStore.getState().activeView).toBe("focus");
});
it("navigates to queue view on Cmd+2", () => {
render(<Navigation />);
act(() => {
dispatchKeyEvent("2", { metaKey: true });
});
expect(useNavStore.getState().activeView).toBe("queue");
});
it("navigates to inbox view on Cmd+3", () => {
render(<Navigation />);
act(() => {
dispatchKeyEvent("3", { metaKey: true });
});
expect(useNavStore.getState().activeView).toBe("inbox");
});
it("navigates to settings view on Cmd+,", () => {
render(<Navigation />);
act(() => {
dispatchKeyEvent(",", { metaKey: true });
});
expect(useNavStore.getState().activeView).toBe("settings");
});
it("displays keyboard shortcut hints", () => {
render(<Navigation />);
// Check for shortcut hints in the nav items
expect(screen.getByText("1")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
});
});