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:
@@ -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" });
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user