feat(KB-188): reorganize header button layout for mobile

- Reorganize Header component button layout for mobile view

- Update Header tests with mobile layout coverage

- Improve responsive button arrangement in mobile breakpoint
This commit is contained in:
gsxdsm
2026-03-30 12:26:33 -07:00
parent f9f432108f
commit c6774d8162
2 changed files with 126 additions and 36 deletions

View File

@@ -1,10 +1,26 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { Header } from "./Header"; import { Header } from "./Header";
const noop = () => {}; const noop = () => {};
function renderHeader(props = {}) { // Helper to mock mobile/desktop viewport
function mockMatchMedia(matches: boolean) {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: matches && query.includes("max-width: 768px"),
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
function renderHeader(props = {}, isMobile = false) {
mockMatchMedia(isMobile);
return render( return render(
<Header <Header
onOpenSettings={noop} onOpenSettings={noop}
@@ -98,20 +114,25 @@ describe("Header", () => {
}); });
describe("terminal button", () => { describe("terminal button", () => {
it("renders terminal button with correct title", () => { it("renders terminal button with correct title on desktop", () => {
renderHeader({ onToggleTerminal: noop }); renderHeader({ onToggleTerminal: noop }, false);
expect(screen.getByTitle("Open Terminal")).toBeDefined(); expect(screen.getByTitle("Open Terminal")).toBeDefined();
}); });
it("does not render terminal button inline on mobile", () => {
renderHeader({ onToggleTerminal: noop }, true);
expect(screen.queryByTitle("Open Terminal")).toBeNull();
});
it("calls onToggleTerminal when terminal button is clicked", () => { it("calls onToggleTerminal when terminal button is clicked", () => {
const onToggleTerminal = vi.fn(); const onToggleTerminal = vi.fn();
renderHeader({ onToggleTerminal }); renderHeader({ onToggleTerminal }, false);
fireEvent.click(screen.getByTitle("Open Terminal")); fireEvent.click(screen.getByTitle("Open Terminal"));
expect(onToggleTerminal).toHaveBeenCalled(); expect(onToggleTerminal).toHaveBeenCalled();
}); });
it("is always enabled regardless of task state", () => { it("is always enabled regardless of task state", () => {
renderHeader({ onToggleTerminal: noop }); renderHeader({ onToggleTerminal: noop }, false);
const btn = screen.getByTitle("Open Terminal"); const btn = screen.getByTitle("Open Terminal");
expect(btn.hasAttribute("disabled")).toBe(false); expect(btn.hasAttribute("disabled")).toBe(false);
}); });
@@ -155,42 +176,108 @@ describe("Header", () => {
describe("usage button", () => { describe("usage button", () => {
it("does not render usage button when onOpenUsage is not provided", () => { it("does not render usage button when onOpenUsage is not provided", () => {
renderHeader(); renderHeader({}, false);
expect(screen.queryByTitle("View usage")).toBeNull(); expect(screen.queryByTitle("View usage")).toBeNull();
}); });
it("renders usage button with correct title when onOpenUsage is provided", () => { it("does not render usage button when onOpenUsage is not provided on mobile", () => {
renderHeader({ onOpenUsage: vi.fn() }); renderHeader({}, true);
expect(screen.queryByTitle("View usage")).toBeNull();
});
it("renders usage button with correct title when onOpenUsage is provided on desktop", () => {
renderHeader({ onOpenUsage: vi.fn() }, false);
expect(screen.getByTitle("View usage")).toBeDefined();
});
it("renders usage button inline on mobile when onOpenUsage is provided", () => {
renderHeader({ onOpenUsage: vi.fn() }, true);
expect(screen.getByTitle("View usage")).toBeDefined(); expect(screen.getByTitle("View usage")).toBeDefined();
}); });
it("calls onOpenUsage when usage button is clicked", () => { it("calls onOpenUsage when usage button is clicked", () => {
const onOpenUsage = vi.fn(); const onOpenUsage = vi.fn();
renderHeader({ onOpenUsage }); renderHeader({ onOpenUsage }, false);
fireEvent.click(screen.getByTitle("View usage"));
expect(onOpenUsage).toHaveBeenCalled();
});
it("calls onOpenUsage when usage button is clicked on mobile", () => {
const onOpenUsage = vi.fn();
renderHeader({ onOpenUsage }, true);
fireEvent.click(screen.getByTitle("View usage")); fireEvent.click(screen.getByTitle("View usage"));
expect(onOpenUsage).toHaveBeenCalled(); expect(onOpenUsage).toHaveBeenCalled();
}); });
}); });
describe("planning button", () => { describe("planning button", () => {
it("renders planning button with correct title", () => { it("renders planning button with correct title on desktop", () => {
renderHeader({ onOpenPlanning: vi.fn() }); renderHeader({ onOpenPlanning: vi.fn() }, false);
expect(screen.getByTitle("Create a task with AI planning")).toBeDefined(); expect(screen.getByTitle("Create a task with AI planning")).toBeDefined();
}); });
it("does not render planning button inline on mobile", () => {
renderHeader({ onOpenPlanning: vi.fn() }, true);
expect(screen.queryByTitle("Create a task with AI planning")).toBeNull();
});
it("calls onOpenPlanning when planning button is clicked", () => { it("calls onOpenPlanning when planning button is clicked", () => {
const onOpenPlanning = vi.fn(); const onOpenPlanning = vi.fn();
renderHeader({ onOpenPlanning }); renderHeader({ onOpenPlanning }, false);
fireEvent.click(screen.getByTitle("Create a task with AI planning")); fireEvent.click(screen.getByTitle("Create a task with AI planning"));
expect(onOpenPlanning).toHaveBeenCalled(); expect(onOpenPlanning).toHaveBeenCalled();
}); });
it("has correct data-testid for testing", () => { it("has correct data-testid for testing on desktop", () => {
renderHeader({ onOpenPlanning: vi.fn() }); renderHeader({ onOpenPlanning: vi.fn() }, false);
expect(screen.getByTestId("planning-btn")).toBeDefined(); expect(screen.getByTestId("planning-btn")).toBeDefined();
}); });
}); });
describe("mobile overflow menu", () => {
it("renders overflow trigger on mobile", () => {
renderHeader({}, true);
expect(screen.getByTitle("More header actions")).toBeDefined();
});
it("does not render overflow trigger on desktop", () => {
renderHeader({}, false);
expect(screen.queryByTitle("More header actions")).toBeNull();
});
it("shows terminal in overflow menu on mobile", () => {
renderHeader({ onToggleTerminal: noop }, true);
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByTestId("overflow-terminal-btn")).toBeDefined();
});
it("shows GitHub import in overflow menu on mobile", () => {
renderHeader({}, true);
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByText("Import from GitHub")).toBeDefined();
});
it("shows planning in overflow menu on mobile", () => {
renderHeader({ onOpenPlanning: noop }, true);
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByTestId("overflow-planning-btn")).toBeDefined();
});
it("shows settings in overflow menu on mobile", () => {
renderHeader({}, true);
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.getByText("Settings")).toBeDefined();
});
it("calls onToggleTerminal when overflow terminal button is clicked", () => {
const onToggleTerminal = vi.fn();
renderHeader({ onToggleTerminal }, true);
fireEvent.click(screen.getByTitle("More header actions"));
fireEvent.click(screen.getByTestId("overflow-terminal-btn"));
expect(onToggleTerminal).toHaveBeenCalled();
});
});
describe("search functionality", () => { describe("search functionality", () => {
it("does not render search input when onSearchChange is not provided", () => { it("does not render search input when onSearchChange is not provided", () => {
renderHeader({ view: "board" }); renderHeader({ view: "board" });

View File

@@ -211,13 +211,14 @@ export function Header({
</> </>
)} )}
{/* Desktop actions */} {/* Usage button - inline on all screens when onOpenUsage provided */}
{!isMobile && onOpenUsage && ( {onOpenUsage && (
<button className="btn-icon" onClick={onOpenUsage} title="View usage"> <button className="btn-icon" onClick={onOpenUsage} title="View usage">
<Activity size={16} /> <Activity size={16} />
</button> </button>
)} )}
{/* Desktop actions */}
{!isMobile && ( {!isMobile && (
<button className="btn-icon" onClick={onOpenGitHubImport} title="Import from GitHub"> <button className="btn-icon" onClick={onOpenGitHubImport} title="Import from GitHub">
<Download size={16} /> <Download size={16} />
@@ -235,15 +236,17 @@ export function Header({
</button> </button>
)} )}
{/* Terminal button - always inline */} {/* Terminal button - desktop only (moved to overflow on mobile) */}
<button {!isMobile && (
className="btn-icon btn-icon--terminal" <button
onClick={onToggleTerminal} className="btn-icon btn-icon--terminal"
title="Open Terminal" onClick={onToggleTerminal}
data-testid="terminal-toggle-btn" title="Open Terminal"
> data-testid="terminal-toggle-btn"
<Terminal size={16} /> >
</button> <Terminal size={16} />
</button>
)}
{/* Pause button (soft pause) - always inline */} {/* Pause button (soft pause) - always inline */}
<button <button
@@ -294,16 +297,16 @@ export function Header({
role="menu" role="menu"
aria-label="Additional header actions" aria-label="Additional header actions"
> >
{onOpenUsage && ( {/* Terminal - in overflow on mobile */}
<button <button
className="mobile-overflow-item" className="mobile-overflow-item"
onClick={() => handleOverflowAction(onOpenUsage)} onClick={() => handleOverflowAction(onToggleTerminal)}
role="menuitem" role="menuitem"
> data-testid="overflow-terminal-btn"
<Activity size={16} /> >
<span>View usage</span> <Terminal size={16} />
</button> <span>Open Terminal</span>
)} </button>
<button <button
className="mobile-overflow-item" className="mobile-overflow-item"
onClick={() => handleOverflowAction(onOpenGitHubImport)} onClick={() => handleOverflowAction(onOpenGitHubImport)}