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:
Fusion
2026-04-19 05:47:36 -07:00
committed by gsxdsm
parent f71c16cd54
commit daeeb2f83d
5 changed files with 144 additions and 88 deletions

View File

@@ -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", () => {

View File

@@ -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>
)}

View File

@@ -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 />);

View File

@@ -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();
});