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 { Header } from "./Header";
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(
<Header
onOpenSettings={noop}
@@ -98,20 +114,25 @@ describe("Header", () => {
});
describe("terminal button", () => {
it("renders terminal button with correct title", () => {
renderHeader({ onToggleTerminal: noop });
it("renders terminal button with correct title on desktop", () => {
renderHeader({ onToggleTerminal: noop }, false);
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", () => {
const onToggleTerminal = vi.fn();
renderHeader({ onToggleTerminal });
renderHeader({ onToggleTerminal }, false);
fireEvent.click(screen.getByTitle("Open Terminal"));
expect(onToggleTerminal).toHaveBeenCalled();
});
it("is always enabled regardless of task state", () => {
renderHeader({ onToggleTerminal: noop });
renderHeader({ onToggleTerminal: noop }, false);
const btn = screen.getByTitle("Open Terminal");
expect(btn.hasAttribute("disabled")).toBe(false);
});
@@ -155,42 +176,108 @@ describe("Header", () => {
describe("usage button", () => {
it("does not render usage button when onOpenUsage is not provided", () => {
renderHeader();
renderHeader({}, false);
expect(screen.queryByTitle("View usage")).toBeNull();
});
it("renders usage button with correct title when onOpenUsage is provided", () => {
renderHeader({ onOpenUsage: vi.fn() });
it("does not render usage button when onOpenUsage is not provided on mobile", () => {
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();
});
it("calls onOpenUsage when usage button is clicked", () => {
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"));
expect(onOpenUsage).toHaveBeenCalled();
});
});
describe("planning button", () => {
it("renders planning button with correct title", () => {
renderHeader({ onOpenPlanning: vi.fn() });
it("renders planning button with correct title on desktop", () => {
renderHeader({ onOpenPlanning: vi.fn() }, false);
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", () => {
const onOpenPlanning = vi.fn();
renderHeader({ onOpenPlanning });
renderHeader({ onOpenPlanning }, false);
fireEvent.click(screen.getByTitle("Create a task with AI planning"));
expect(onOpenPlanning).toHaveBeenCalled();
});
it("has correct data-testid for testing", () => {
renderHeader({ onOpenPlanning: vi.fn() });
it("has correct data-testid for testing on desktop", () => {
renderHeader({ onOpenPlanning: vi.fn() }, false);
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", () => {
it("does not render search input when onSearchChange is not provided", () => {
renderHeader({ view: "board" });

View File

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