- Reorder Header overflow menu items so Settings always appears last - Update Header.tsx to position SettingsMenuItem as the final menu entry - Add regression tests in Header.test.tsx verifying Settings is last in all overflow states - Add ordering tests in tablet-header-controls.test.tsx for tablet layout
626 lines
24 KiB
TypeScript
626 lines
24 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
import { Header } from "./Header";
|
|
|
|
const noop = () => {};
|
|
|
|
// Helper to mock mobile/tablet/desktop viewport
|
|
type ViewportTier = "mobile" | "tablet" | "desktop";
|
|
|
|
function mockMatchMedia(tier: ViewportTier) {
|
|
Object.defineProperty(window, "matchMedia", {
|
|
writable: true,
|
|
value: vi.fn().mockImplementation((query: string) => {
|
|
let matches = false;
|
|
if (tier === "mobile" && query.includes("max-width: 768px")) {
|
|
matches = true;
|
|
} else if (tier === "tablet" && query.includes("769px") && query.includes("1024px")) {
|
|
matches = true;
|
|
}
|
|
// desktop: neither mobile nor tablet query matches
|
|
return {
|
|
matches,
|
|
media: query,
|
|
onchange: null,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
function renderHeader(props = {}, tier: ViewportTier = "desktop") {
|
|
mockMatchMedia(tier);
|
|
return render(
|
|
<Header
|
|
onOpenSettings={noop}
|
|
onOpenGitHubImport={noop}
|
|
globalPaused={false}
|
|
enginePaused={false}
|
|
onToggleGlobalPause={noop}
|
|
onToggleEnginePause={noop}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
describe("Header", () => {
|
|
it("renders the logo and brand", () => {
|
|
renderHeader();
|
|
expect(screen.getByText("Fusion")).toBeDefined();
|
|
});
|
|
|
|
it("renders action buttons", () => {
|
|
renderHeader();
|
|
expect(screen.getByTitle("Import from GitHub")).toBeDefined();
|
|
expect(screen.getByTitle("Settings")).toBeDefined();
|
|
});
|
|
|
|
it("calls onOpenSettings when settings button is clicked", () => {
|
|
const onOpenSettings = vi.fn();
|
|
renderHeader({ onOpenSettings });
|
|
fireEvent.click(screen.getByTitle("Settings"));
|
|
expect(onOpenSettings).toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls onOpenGitHubImport when import button is clicked", () => {
|
|
const onOpenGitHubImport = vi.fn();
|
|
renderHeader({ onOpenGitHubImport });
|
|
fireEvent.click(screen.getByTitle("Import from GitHub"));
|
|
expect(onOpenGitHubImport).toHaveBeenCalled();
|
|
});
|
|
|
|
describe("view toggle", () => {
|
|
it("does not render view toggle when onChangeView is not provided", () => {
|
|
renderHeader();
|
|
expect(screen.queryByTitle("Board view")).toBeNull();
|
|
expect(screen.queryByTitle("List view")).toBeNull();
|
|
});
|
|
|
|
it("renders view toggle when onChangeView is provided", () => {
|
|
renderHeader({ onChangeView: noop });
|
|
expect(screen.getByTitle("Board view")).toBeDefined();
|
|
expect(screen.getByTitle("List view")).toBeDefined();
|
|
});
|
|
|
|
it("shows board view as active by default", () => {
|
|
renderHeader({ onChangeView: noop });
|
|
const boardBtn = screen.getByTitle("Board view");
|
|
const listBtn = screen.getByTitle("List view");
|
|
expect(boardBtn.className).toContain("active");
|
|
expect(listBtn.className).not.toContain("active");
|
|
});
|
|
|
|
it("shows list view as active when view is 'list'", () => {
|
|
renderHeader({ onChangeView: noop, view: "list" });
|
|
const boardBtn = screen.getByTitle("Board view");
|
|
const listBtn = screen.getByTitle("List view");
|
|
expect(boardBtn.className).not.toContain("active");
|
|
expect(listBtn.className).toContain("active");
|
|
});
|
|
|
|
it("calls onChangeView with 'board' when clicking board view button", () => {
|
|
const onChangeView = vi.fn();
|
|
renderHeader({ onChangeView, view: "list" });
|
|
fireEvent.click(screen.getByTitle("Board view"));
|
|
expect(onChangeView).toHaveBeenCalledWith("board");
|
|
});
|
|
|
|
it("calls onChangeView with 'list' when clicking list view button", () => {
|
|
const onChangeView = vi.fn();
|
|
renderHeader({ onChangeView, view: "board" });
|
|
fireEvent.click(screen.getByTitle("List view"));
|
|
expect(onChangeView).toHaveBeenCalledWith("list");
|
|
});
|
|
|
|
it("has correct aria attributes for accessibility", () => {
|
|
renderHeader({ onChangeView: noop, view: "board" });
|
|
const boardBtn = screen.getByTitle("Board view");
|
|
const listBtn = screen.getByTitle("List view");
|
|
expect(boardBtn.getAttribute("aria-pressed")).toBe("true");
|
|
expect(listBtn.getAttribute("aria-pressed")).toBe("false");
|
|
});
|
|
});
|
|
|
|
describe("terminal button", () => {
|
|
it("renders terminal button with correct title on desktop", () => {
|
|
renderHeader({ onToggleTerminal: noop }, "desktop");
|
|
expect(screen.getByTitle("Open Terminal")).toBeDefined();
|
|
});
|
|
|
|
it("does not render terminal button inline on mobile", () => {
|
|
renderHeader({ onToggleTerminal: noop }, "mobile");
|
|
expect(screen.queryByTitle("Open Terminal")).toBeNull();
|
|
});
|
|
|
|
it("calls onToggleTerminal when terminal button is clicked", () => {
|
|
const onToggleTerminal = vi.fn();
|
|
renderHeader({ onToggleTerminal }, "desktop");
|
|
fireEvent.click(screen.getByTitle("Open Terminal"));
|
|
expect(onToggleTerminal).toHaveBeenCalled();
|
|
});
|
|
|
|
it("is always enabled regardless of task state", () => {
|
|
renderHeader({ onToggleTerminal: noop }, "desktop");
|
|
const btn = screen.getByTitle("Open Terminal");
|
|
expect(btn.hasAttribute("disabled")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("files button", () => {
|
|
it("renders files button on desktop when handler is provided", () => {
|
|
renderHeader({ onOpenFiles: vi.fn() }, "desktop");
|
|
expect(screen.getByTitle("Browse files")).toBeDefined();
|
|
});
|
|
|
|
it("does not render files button on desktop when handler is omitted", () => {
|
|
renderHeader({}, "desktop");
|
|
expect(screen.queryByTitle("Browse files")).toBeNull();
|
|
});
|
|
|
|
it("calls onOpenFiles when desktop files button is clicked", () => {
|
|
const onOpenFiles = vi.fn();
|
|
renderHeader({ onOpenFiles }, "desktop");
|
|
fireEvent.click(screen.getByTitle("Browse files"));
|
|
expect(onOpenFiles).toHaveBeenCalled();
|
|
});
|
|
|
|
it("applies active class when files modal is open", () => {
|
|
renderHeader({ onOpenFiles: vi.fn(), filesOpen: true }, "desktop");
|
|
expect(screen.getByTitle("Browse files").className).toContain("btn-icon--active");
|
|
});
|
|
|
|
it("shows files action in mobile overflow menu", () => {
|
|
renderHeader({ onOpenFiles: vi.fn() }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByTestId("overflow-files-btn")).toBeDefined();
|
|
});
|
|
|
|
it("calls onOpenFiles from mobile overflow menu", () => {
|
|
const onOpenFiles = vi.fn();
|
|
renderHeader({ onOpenFiles }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
fireEvent.click(screen.getByTestId("overflow-files-btn"));
|
|
expect(onOpenFiles).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("pause controls", () => {
|
|
it("renders pause button for engine pause", () => {
|
|
renderHeader();
|
|
expect(screen.getByTitle("Pause scheduling")).toBeDefined();
|
|
});
|
|
|
|
it("renders stop button for global pause", () => {
|
|
renderHeader();
|
|
expect(screen.getByTitle("Stop AI engine")).toBeDefined();
|
|
});
|
|
|
|
it("calls onToggleEnginePause when pause button is clicked", () => {
|
|
const onToggleEnginePause = vi.fn();
|
|
renderHeader({ onToggleEnginePause });
|
|
fireEvent.click(screen.getByTitle("Pause scheduling"));
|
|
expect(onToggleEnginePause).toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls onToggleGlobalPause when stop button is clicked", () => {
|
|
const onToggleGlobalPause = vi.fn();
|
|
renderHeader({ onToggleGlobalPause });
|
|
fireEvent.click(screen.getByTitle("Stop AI engine"));
|
|
expect(onToggleGlobalPause).toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows resume text when engine is paused", () => {
|
|
renderHeader({ enginePaused: true });
|
|
expect(screen.getByTitle("Resume scheduling")).toBeDefined();
|
|
});
|
|
|
|
it("shows start text when global is paused", () => {
|
|
renderHeader({ globalPaused: true });
|
|
expect(screen.getByTitle("Start AI engine")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("usage button", () => {
|
|
it("does not render usage button when onOpenUsage is not provided", () => {
|
|
renderHeader({}, "desktop");
|
|
expect(screen.queryByTitle("View usage")).toBeNull();
|
|
});
|
|
|
|
it("does not render usage button when onOpenUsage is not provided on mobile", () => {
|
|
renderHeader({}, "mobile");
|
|
expect(screen.queryByTitle("View usage")).toBeNull();
|
|
});
|
|
|
|
it("renders usage button with correct title when onOpenUsage is provided on desktop", () => {
|
|
renderHeader({ onOpenUsage: vi.fn() }, "desktop");
|
|
expect(screen.getByTitle("View usage")).toBeDefined();
|
|
});
|
|
|
|
it("does not render usage button inline on mobile when onOpenUsage is provided", () => {
|
|
renderHeader({ onOpenUsage: vi.fn() }, "mobile");
|
|
// Button should NOT be inline on mobile (it's in overflow menu)
|
|
expect(screen.queryByTitle("View usage")).toBeNull();
|
|
});
|
|
|
|
it("shows usage in overflow menu on mobile", () => {
|
|
renderHeader({ onOpenUsage: vi.fn() }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByTestId("overflow-usage-btn")).toBeDefined();
|
|
});
|
|
|
|
it("calls onOpenUsage when usage button is clicked on desktop", () => {
|
|
const onOpenUsage = vi.fn();
|
|
renderHeader({ onOpenUsage }, "desktop");
|
|
fireEvent.click(screen.getByTitle("View usage"));
|
|
expect(onOpenUsage).toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls onOpenUsage when usage button in overflow menu is clicked", () => {
|
|
const onOpenUsage = vi.fn();
|
|
renderHeader({ onOpenUsage }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
fireEvent.click(screen.getByTestId("overflow-usage-btn"));
|
|
expect(onOpenUsage).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("activity log button", () => {
|
|
it("does not render activity log button when onOpenActivityLog is not provided", () => {
|
|
renderHeader({}, "desktop");
|
|
expect(screen.queryByTitle("View Activity Log")).toBeNull();
|
|
});
|
|
|
|
it("does not render activity log button when onOpenActivityLog is not provided on mobile", () => {
|
|
renderHeader({}, "mobile");
|
|
expect(screen.queryByTitle("View Activity Log")).toBeNull();
|
|
});
|
|
|
|
it("renders activity log button with correct title when onOpenActivityLog is provided on desktop", () => {
|
|
renderHeader({ onOpenActivityLog: vi.fn() }, "desktop");
|
|
expect(screen.getByTitle("View Activity Log")).toBeDefined();
|
|
});
|
|
|
|
it("does not render activity log button inline on mobile when onOpenActivityLog is provided", () => {
|
|
renderHeader({ onOpenActivityLog: vi.fn() }, "mobile");
|
|
// Button should NOT be inline on mobile (it's in overflow menu)
|
|
expect(screen.queryByTitle("View Activity Log")).toBeNull();
|
|
});
|
|
|
|
it("shows activity log in overflow menu on mobile", () => {
|
|
renderHeader({ onOpenActivityLog: vi.fn() }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByTestId("overflow-activity-log-btn")).toBeDefined();
|
|
});
|
|
|
|
it("calls onOpenActivityLog when activity log button is clicked on desktop", () => {
|
|
const onOpenActivityLog = vi.fn();
|
|
renderHeader({ onOpenActivityLog }, "desktop");
|
|
fireEvent.click(screen.getByTitle("View Activity Log"));
|
|
expect(onOpenActivityLog).toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls onOpenActivityLog when activity log button in overflow menu is clicked", () => {
|
|
const onOpenActivityLog = vi.fn();
|
|
renderHeader({ onOpenActivityLog }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
fireEvent.click(screen.getByTestId("overflow-activity-log-btn"));
|
|
expect(onOpenActivityLog).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("planning button", () => {
|
|
it("renders planning button with correct title on desktop", () => {
|
|
renderHeader({ onOpenPlanning: vi.fn() }, "desktop");
|
|
expect(screen.getByTitle("Create a task with AI planning")).toBeDefined();
|
|
});
|
|
|
|
it("does not render planning button inline on mobile", () => {
|
|
renderHeader({ onOpenPlanning: vi.fn() }, "mobile");
|
|
expect(screen.queryByTitle("Create a task with AI planning")).toBeNull();
|
|
});
|
|
|
|
it("calls onOpenPlanning when planning button is clicked", () => {
|
|
const onOpenPlanning = vi.fn();
|
|
renderHeader({ onOpenPlanning }, "desktop");
|
|
fireEvent.click(screen.getByTitle("Create a task with AI planning"));
|
|
expect(onOpenPlanning).toHaveBeenCalled();
|
|
});
|
|
|
|
it("has correct data-testid for testing on desktop", () => {
|
|
renderHeader({ onOpenPlanning: vi.fn() }, "desktop");
|
|
expect(screen.getByTestId("planning-btn")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("mobile overflow menu", () => {
|
|
it("renders overflow trigger on mobile", () => {
|
|
renderHeader({}, "mobile");
|
|
expect(screen.getByTitle("More header actions")).toBeDefined();
|
|
});
|
|
|
|
it("does not render overflow trigger on desktop", () => {
|
|
renderHeader({}, "desktop");
|
|
expect(screen.queryByTitle("More header actions")).toBeNull();
|
|
});
|
|
|
|
it("shows terminal in overflow menu on mobile", () => {
|
|
renderHeader({ onToggleTerminal: noop }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByTestId("overflow-terminal-btn")).toBeDefined();
|
|
});
|
|
|
|
it("shows GitHub import in overflow menu on mobile", () => {
|
|
renderHeader({}, "mobile");
|
|
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 }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByTestId("overflow-planning-btn")).toBeDefined();
|
|
});
|
|
|
|
it("shows settings in overflow menu on mobile", () => {
|
|
renderHeader({}, "mobile");
|
|
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 }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
fireEvent.click(screen.getByTestId("overflow-terminal-btn"));
|
|
expect(onToggleTerminal).toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows scripts in overflow menu on mobile", () => {
|
|
renderHeader({ onOpenScripts: noop }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByTestId("overflow-scripts-btn")).toBeDefined();
|
|
});
|
|
|
|
it("calls onOpenScripts from mobile overflow menu", () => {
|
|
const onOpenScripts = vi.fn();
|
|
renderHeader({ onOpenScripts }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
fireEvent.click(screen.getByTestId("overflow-scripts-btn"));
|
|
expect(onOpenScripts).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("search functionality", () => {
|
|
it("does not render search input when onSearchChange is not provided", () => {
|
|
renderHeader({ view: "board" });
|
|
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
|
});
|
|
|
|
it("renders search input when onSearchChange and view='board' are provided", () => {
|
|
renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
|
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
|
});
|
|
|
|
it("does not render search input when view is 'list'", () => {
|
|
renderHeader({ onSearchChange: vi.fn(), view: "list" });
|
|
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
|
});
|
|
|
|
it("calls onSearchChange when typing in search input", () => {
|
|
const onSearchChange = vi.fn();
|
|
renderHeader({ onSearchChange, view: "board" });
|
|
const input = screen.getByPlaceholderText("Search tasks...");
|
|
fireEvent.change(input, { target: { value: "test query" } });
|
|
expect(onSearchChange).toHaveBeenCalledWith("test query");
|
|
});
|
|
|
|
it("shows clear button when search query is not empty", () => {
|
|
renderHeader({ onSearchChange: vi.fn(), view: "board", searchQuery: "test" });
|
|
expect(screen.getByLabelText("Clear search")).toBeDefined();
|
|
});
|
|
|
|
it("does not show clear button when search query is empty", () => {
|
|
renderHeader({ onSearchChange: vi.fn(), view: "board", searchQuery: "" });
|
|
expect(screen.queryByLabelText("Clear search")).toBeNull();
|
|
});
|
|
|
|
it("calls onSearchChange with empty string when clear button is clicked", () => {
|
|
const onSearchChange = vi.fn();
|
|
renderHeader({ onSearchChange, view: "board", searchQuery: "test" });
|
|
fireEvent.click(screen.getByLabelText("Clear search"));
|
|
expect(onSearchChange).toHaveBeenCalledWith("");
|
|
});
|
|
|
|
it("search input has correct placeholder text", () => {
|
|
renderHeader({ onSearchChange: vi.fn(), view: "board" });
|
|
const input = screen.getByPlaceholderText("Search tasks...");
|
|
expect(input).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("schedules button", () => {
|
|
it("renders schedules button on desktop", () => {
|
|
renderHeader({ onOpenSchedules: vi.fn() }, "desktop");
|
|
expect(screen.getByTitle("Scheduled tasks")).toBeDefined();
|
|
});
|
|
|
|
it("does not render schedules button inline on mobile", () => {
|
|
renderHeader({ onOpenSchedules: vi.fn() }, "mobile");
|
|
expect(screen.queryByTitle("Scheduled tasks")).toBeNull();
|
|
});
|
|
|
|
it("calls onOpenSchedules when schedules button is clicked", () => {
|
|
const onOpenSchedules = vi.fn();
|
|
renderHeader({ onOpenSchedules }, "desktop");
|
|
fireEvent.click(screen.getByTitle("Scheduled tasks"));
|
|
expect(onOpenSchedules).toHaveBeenCalled();
|
|
});
|
|
|
|
it("has correct data-testid for testing on desktop", () => {
|
|
renderHeader({ onOpenSchedules: vi.fn() }, "desktop");
|
|
expect(screen.getByTestId("schedules-btn")).toBeDefined();
|
|
});
|
|
|
|
it("includes scheduled tasks in overflow menu on mobile", () => {
|
|
renderHeader({ onOpenSchedules: vi.fn() }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByText("Scheduled Tasks")).toBeDefined();
|
|
});
|
|
|
|
it("calls onOpenSchedules from mobile overflow menu", () => {
|
|
const onOpenSchedules = vi.fn();
|
|
renderHeader({ onOpenSchedules }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
fireEvent.click(screen.getByTestId("overflow-schedules-btn"));
|
|
expect(onOpenSchedules).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("mobile header layout", () => {
|
|
it("applies header-project-selector class when multiple projects exist on mobile", () => {
|
|
const { container } = renderHeader({
|
|
projects: [
|
|
{ id: "1", name: "Project One", path: "/path/one", status: "active" as const },
|
|
{ id: "2", name: "Project Two", path: "/path/two", status: "active" as const },
|
|
],
|
|
currentProject: { id: "1", name: "Project One", path: "/path/one", status: "active" as const },
|
|
}, true);
|
|
expect(container.querySelector(".header-project-selector")).toBeDefined();
|
|
});
|
|
|
|
it("does not show project selector on mobile with single project", () => {
|
|
const { container } = renderHeader({
|
|
projects: [{ id: "1", name: "Project One", path: "/path/one", status: "active" as const }],
|
|
}, true);
|
|
expect(container.querySelector(".header-project-selector")).toBeNull();
|
|
});
|
|
|
|
it("renders header-back-button when currentProject is set on mobile", () => {
|
|
const { container } = renderHeader({
|
|
currentProject: { id: "1", name: "Project One", path: "/path/one", status: "active" as const },
|
|
onViewAllProjects: vi.fn(),
|
|
}, true);
|
|
expect(container.querySelector(".header-back-button")).toBeDefined();
|
|
});
|
|
|
|
it("does not render header-back-button on mobile when no currentProject", () => {
|
|
const { container } = renderHeader({}, "mobile");
|
|
expect(container.querySelector(".header-back-button")).toBeNull();
|
|
});
|
|
|
|
it("mobile overflow menu closes when clicking outside", () => {
|
|
renderHeader({ onOpenFiles: vi.fn() }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByRole("menu")).toBeDefined();
|
|
|
|
// Click outside the menu
|
|
fireEvent.mouseDown(document.body);
|
|
expect(screen.queryByRole("menu")).toBeNull();
|
|
});
|
|
|
|
it("mobile overflow menu closes on Escape key", () => {
|
|
renderHeader({ onOpenFiles: vi.fn() }, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
expect(screen.getByRole("menu")).toBeDefined();
|
|
|
|
fireEvent.keyDown(document, { key: "Escape" });
|
|
expect(screen.queryByRole("menu")).toBeNull();
|
|
});
|
|
|
|
it("mobile overflow trigger has correct accessibility attributes", () => {
|
|
renderHeader({}, "mobile");
|
|
const trigger = screen.getByTitle("More header actions");
|
|
expect(trigger.getAttribute("aria-haspopup")).toBe("menu");
|
|
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
|
|
fireEvent.click(trigger);
|
|
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
});
|
|
|
|
it("hides logo-sub on mobile via CSS", () => {
|
|
renderHeader({}, "mobile");
|
|
// The "tasks" element no longer exists - it was removed
|
|
});
|
|
});
|
|
|
|
describe("action ordering", () => {
|
|
it("Settings is the last inline user-facing action on desktop (before pause/stop)", () => {
|
|
const { container } = renderHeader({
|
|
onOpenUsage: noop,
|
|
onOpenActivityLog: noop,
|
|
onOpenWorkflowSteps: noop,
|
|
onOpenMissions: noop,
|
|
onOpenFiles: noop,
|
|
onOpenGitManager: noop,
|
|
onOpenScripts: noop,
|
|
onRunScript: noop,
|
|
}, "desktop");
|
|
|
|
// Get all inline btn-icon buttons inside header-actions
|
|
const headerActions = container.querySelector(".header-actions")!;
|
|
const inlineButtons = Array.from(headerActions.querySelectorAll<HTMLButtonElement>(":scope > button.btn-icon"));
|
|
|
|
// Find the Settings button index and the Pause/Stop button indices
|
|
const settingsIdx = inlineButtons.findIndex((btn) => btn.title === "Settings");
|
|
const pauseIdx = inlineButtons.findIndex((btn) => btn.title === "Pause scheduling" || btn.title === "Resume scheduling");
|
|
const stopIdx = inlineButtons.findIndex((btn) => btn.title === "Stop AI engine" || btn.title === "Start AI engine");
|
|
|
|
// Settings must exist
|
|
expect(settingsIdx).toBeGreaterThanOrEqual(0);
|
|
|
|
// Settings must come before pause/stop (engine controls come after Settings)
|
|
if (pauseIdx >= 0) {
|
|
expect(settingsIdx).toBeLessThan(pauseIdx);
|
|
}
|
|
if (stopIdx >= 0) {
|
|
expect(settingsIdx).toBeLessThan(stopIdx);
|
|
}
|
|
|
|
// Settings must be the last button before pause/stop — no other user-facing btn-icon after it
|
|
const buttonsAfterSettings = inlineButtons.slice(settingsIdx + 1);
|
|
const userFacingAfterSettings = buttonsAfterSettings.filter(
|
|
(btn) => btn.title !== "Pause scheduling" &&
|
|
btn.title !== "Resume scheduling" &&
|
|
btn.title !== "Stop AI engine" &&
|
|
btn.title !== "Start AI engine"
|
|
);
|
|
expect(userFacingAfterSettings).toHaveLength(0);
|
|
});
|
|
|
|
it("Settings is the last item in the mobile overflow menu", () => {
|
|
const { container } = renderHeader({
|
|
onOpenUsage: noop,
|
|
onOpenActivityLog: noop,
|
|
onOpenWorkflowSteps: noop,
|
|
onOpenMissions: noop,
|
|
onOpenFiles: noop,
|
|
onOpenGitManager: noop,
|
|
}, "mobile");
|
|
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
|
|
// Get all menu items inside the overflow menu
|
|
const menu = container.querySelector(".mobile-overflow-menu")!;
|
|
const menuItems = Array.from(menu.querySelectorAll<HTMLButtonElement>("button.mobile-overflow-item"));
|
|
|
|
// The last menu item should be Settings
|
|
const lastItem = menuItems[menuItems.length - 1];
|
|
expect(lastItem.textContent).toBe("Settings");
|
|
});
|
|
|
|
it("Settings is the last item in the mobile overflow menu even when optional items are absent", () => {
|
|
renderHeader({}, "mobile");
|
|
fireEvent.click(screen.getByTitle("More header actions"));
|
|
|
|
// Get the overflow menu items
|
|
const menu = screen.getByRole("menu");
|
|
const menuItems = Array.from(menu.querySelectorAll<HTMLButtonElement>("button[role='menuitem']"));
|
|
|
|
const lastItem = menuItems[menuItems.length - 1];
|
|
expect(lastItem.textContent).toBe("Settings");
|
|
});
|
|
});
|
|
});
|