fix(FN-2143): hide empty header view overflow trigger
- Gate the view overflow trigger and menu behind a computed hasViewOverflowItems check - Render overflow controls only when at least one of Insights, Roadmaps, Skills, or Memory is enabled - Add Header tests covering trigger visibility for enabled and fully disabled overflow configurations - Align App and tablet header tests to include an available overflow item when validating hidden menu entries
This commit is contained in:
@@ -133,6 +133,30 @@ describe("Header", () => {
|
||||
expect(boardBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
expect(listBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
it("does not render view overflow trigger when no overflow items are enabled", () => {
|
||||
renderHeader({ onChangeView: noop });
|
||||
expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders view overflow trigger when an experimental overflow feature is enabled", () => {
|
||||
renderHeader({ onChangeView: noop, experimentalFeatures: { insights: true } });
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders view overflow trigger when skills tab is enabled", () => {
|
||||
renderHeader({ onChangeView: noop, showSkillsTab: true });
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not render view overflow trigger when overflow feature flags are explicitly false", () => {
|
||||
renderHeader({
|
||||
onChangeView: noop,
|
||||
showSkillsTab: false,
|
||||
experimentalFeatures: { insights: false, roadmap: false, memoryView: false },
|
||||
});
|
||||
expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("terminal button", () => {
|
||||
|
||||
@@ -296,6 +296,15 @@ export function Header({
|
||||
return Object.entries(overflowScripts).sort(([a], [b]) => a.localeCompare(b));
|
||||
}, [overflowScripts]);
|
||||
|
||||
const hasViewOverflowItems = useMemo(() => {
|
||||
return !!(
|
||||
experimentalFeatures?.insights ||
|
||||
experimentalFeatures?.roadmap ||
|
||||
showSkillsTab ||
|
||||
experimentalFeatures?.memoryView
|
||||
);
|
||||
}, [experimentalFeatures, showSkillsTab]);
|
||||
|
||||
// Keep ref in sync with state
|
||||
useEffect(() => {
|
||||
terminalSubmenuOpenRef.current = isTerminalSubmenuOpen;
|
||||
@@ -790,82 +799,86 @@ export function Header({
|
||||
>
|
||||
<Mail size={16} />
|
||||
</button>
|
||||
<button
|
||||
ref={viewOverflowTriggerRef}
|
||||
className={`view-toggle-btn${["skills", "roadmaps", "insights"].includes(view) ? " active" : ""}`}
|
||||
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
|
||||
title="More views"
|
||||
aria-label="More views"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isViewOverflowOpen}
|
||||
data-testid="view-toggle-overflow-trigger"
|
||||
>
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
{isViewOverflowOpen && (
|
||||
<div
|
||||
ref={viewOverflowRef}
|
||||
className="view-toggle-overflow-menu"
|
||||
role="menu"
|
||||
aria-label="More views"
|
||||
>
|
||||
{experimentalFeatures?.insights && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "insights" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("insights");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-insights"
|
||||
{hasViewOverflowItems && (
|
||||
<>
|
||||
<button
|
||||
ref={viewOverflowTriggerRef}
|
||||
className={`view-toggle-btn${["skills", "roadmaps", "insights"].includes(view) ? " active" : ""}`}
|
||||
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
|
||||
title="More views"
|
||||
aria-label="More views"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isViewOverflowOpen}
|
||||
data-testid="view-toggle-overflow-trigger"
|
||||
>
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
{isViewOverflowOpen && (
|
||||
<div
|
||||
ref={viewOverflowRef}
|
||||
className="view-toggle-overflow-menu"
|
||||
role="menu"
|
||||
aria-label="More views"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
<span>Insights</span>
|
||||
</button>
|
||||
{experimentalFeatures?.insights && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "insights" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("insights");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-insights"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
<span>Insights</span>
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "roadmaps" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("roadmaps");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-roadmaps"
|
||||
>
|
||||
<Map size={14} />
|
||||
<span>Roadmaps</span>
|
||||
</button>
|
||||
)}
|
||||
{showSkillsTab && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "skills" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("skills");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-skills"
|
||||
>
|
||||
<Zap size={14} />
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.memoryView && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "memory" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("memory");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-toggle-memory"
|
||||
>
|
||||
<Brain size={14} />
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "roadmaps" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("roadmaps");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-roadmaps"
|
||||
>
|
||||
<Map size={14} />
|
||||
<span>Roadmaps</span>
|
||||
</button>
|
||||
)}
|
||||
{showSkillsTab && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "skills" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("skills");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-skills"
|
||||
>
|
||||
<Zap size={14} />
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.memoryView && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "memory" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("memory");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-toggle-memory"
|
||||
>
|
||||
<Brain size={14} />
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1354,10 +1354,10 @@ describe("App view switching", () => {
|
||||
});
|
||||
|
||||
it("does not render insights view button when insights experimental feature is disabled", async () => {
|
||||
// Override fetchSettings to return insights as disabled
|
||||
// Keep at least one overflow item enabled so the overflow trigger still renders.
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { insights: false },
|
||||
experimentalFeatures: { insights: false, roadmap: true },
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
@@ -1373,10 +1373,10 @@ describe("App view switching", () => {
|
||||
});
|
||||
|
||||
it("does not render memory view button when memoryView experimental feature is disabled", async () => {
|
||||
// Override fetchSettings to return memoryView as disabled
|
||||
// Keep another overflow item enabled so the overflow trigger still renders.
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { memoryView: false },
|
||||
experimentalFeatures: { memoryView: false, insights: true },
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
@@ -361,9 +361,9 @@ describe("Header", () => {
|
||||
// ── View Toggle Overflow ─────────────────────────────────────────
|
||||
|
||||
describe("View Toggle Overflow", () => {
|
||||
it("renders overflow trigger button when onChangeView is provided", () => {
|
||||
it("renders overflow trigger button when an overflow item is available", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -436,13 +436,13 @@ describe("Header", () => {
|
||||
|
||||
it("overflow trigger is not active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
|
||||
});
|
||||
|
||||
it("overflow trigger is not active when view is 'list'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="list" onChangeView={onChangeView} />);
|
||||
render(<Header view="list" onChangeView={onChangeView} showSkillsTab />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
|
||||
});
|
||||
|
||||
@@ -477,28 +477,47 @@ describe("Header", () => {
|
||||
|
||||
it("does not render Insights overflow item when experimentalFeatures.insights is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: false }} />);
|
||||
render(
|
||||
<Header
|
||||
view="board"
|
||||
onChangeView={onChangeView}
|
||||
experimentalFeatures={{ insights: false, roadmap: true }}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render Roadmaps overflow item when experimentalFeatures.roadmap is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: false }} />);
|
||||
render(
|
||||
<Header
|
||||
view="board"
|
||||
onChangeView={onChangeView}
|
||||
experimentalFeatures={{ insights: true, roadmap: false }}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-roadmaps")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render Skills overflow item when showSkillsTab is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={false} />);
|
||||
render(
|
||||
<Header
|
||||
view="board"
|
||||
onChangeView={onChangeView}
|
||||
showSkillsTab={false}
|
||||
experimentalFeatures={{ insights: true }}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-skills")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render memory overflow item when memoryView is not enabled", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{}} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-toggle-memory")).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user