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

@@ -104,8 +104,8 @@ describe("tablet header controls", () => {
expect(screen.queryByTitle("Insights view")).toBeNull(); expect(screen.queryByTitle("Insights view")).toBeNull();
}); });
it("renders view toggle overflow trigger on tablet", () => { it("renders view toggle overflow trigger on tablet when overflow items are available", () => {
renderTabletHeader({ onChangeView: noop }); renderTabletHeader({ onChangeView: noop, experimentalFeatures: { insights: true } });
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined(); expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
}); });

View File

@@ -133,6 +133,30 @@ describe("Header", () => {
expect(boardBtn.getAttribute("aria-pressed")).toBe("true"); expect(boardBtn.getAttribute("aria-pressed")).toBe("true");
expect(listBtn.getAttribute("aria-pressed")).toBe("false"); 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", () => { describe("terminal button", () => {

View File

@@ -296,6 +296,15 @@ export function Header({
return Object.entries(overflowScripts).sort(([a], [b]) => a.localeCompare(b)); return Object.entries(overflowScripts).sort(([a], [b]) => a.localeCompare(b));
}, [overflowScripts]); }, [overflowScripts]);
const hasViewOverflowItems = useMemo(() => {
return !!(
experimentalFeatures?.insights ||
experimentalFeatures?.roadmap ||
showSkillsTab ||
experimentalFeatures?.memoryView
);
}, [experimentalFeatures, showSkillsTab]);
// Keep ref in sync with state // Keep ref in sync with state
useEffect(() => { useEffect(() => {
terminalSubmenuOpenRef.current = isTerminalSubmenuOpen; terminalSubmenuOpenRef.current = isTerminalSubmenuOpen;
@@ -790,82 +799,86 @@ export function Header({
> >
<Mail size={16} /> <Mail size={16} />
</button> </button>
<button {hasViewOverflowItems && (
ref={viewOverflowTriggerRef} <>
className={`view-toggle-btn${["skills", "roadmaps", "insights"].includes(view) ? " active" : ""}`} <button
onClick={() => setIsViewOverflowOpen((prev) => !prev)} ref={viewOverflowTriggerRef}
title="More views" className={`view-toggle-btn${["skills", "roadmaps", "insights"].includes(view) ? " active" : ""}`}
aria-label="More views" onClick={() => setIsViewOverflowOpen((prev) => !prev)}
aria-haspopup="menu" title="More views"
aria-expanded={isViewOverflowOpen} aria-label="More views"
data-testid="view-toggle-overflow-trigger" aria-haspopup="menu"
> aria-expanded={isViewOverflowOpen}
<ChevronDown size={12} /> data-testid="view-toggle-overflow-trigger"
</button> >
{isViewOverflowOpen && ( <ChevronDown size={12} />
<div </button>
ref={viewOverflowRef} {isViewOverflowOpen && (
className="view-toggle-overflow-menu" <div
role="menu" ref={viewOverflowRef}
aria-label="More views" className="view-toggle-overflow-menu"
> role="menu"
{experimentalFeatures?.insights && ( aria-label="More views"
<button
className={`view-toggle-overflow-item${view === "insights" ? " active" : ""}`}
onClick={() => {
onChangeView("insights");
setIsViewOverflowOpen(false);
}}
role="menuitem"
data-testid="view-overflow-insights"
> >
<Sparkles size={14} /> {experimentalFeatures?.insights && (
<span>Insights</span> <button
</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> </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 () => { 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({ (fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings, ...defaultSettings,
experimentalFeatures: { insights: false }, experimentalFeatures: { insights: false, roadmap: true },
}); });
render(<App />); render(<App />);
@@ -1373,10 +1373,10 @@ describe("App view switching", () => {
}); });
it("does not render memory view button when memoryView experimental feature is disabled", async () => { 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({ (fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings, ...defaultSettings,
experimentalFeatures: { memoryView: false }, experimentalFeatures: { memoryView: false, insights: true },
}); });
render(<App />); render(<App />);

View File

@@ -361,9 +361,9 @@ describe("Header", () => {
// ── View Toggle Overflow ───────────────────────────────────────── // ── View Toggle Overflow ─────────────────────────────────────────
describe("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(); 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(); expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
}); });
@@ -436,13 +436,13 @@ describe("Header", () => {
it("overflow trigger is not active when view is 'board'", () => { it("overflow trigger is not active when view is 'board'", () => {
const onChangeView = vi.fn(); 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"); expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
}); });
it("overflow trigger is not active when view is 'list'", () => { it("overflow trigger is not active when view is 'list'", () => {
const onChangeView = vi.fn(); 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"); 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", () => { it("does not render Insights overflow item when experimentalFeatures.insights is false", () => {
const onChangeView = vi.fn(); 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")); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-insights")).toBeNull(); expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
}); });
it("does not render Roadmaps overflow item when experimentalFeatures.roadmap is false", () => { it("does not render Roadmaps overflow item when experimentalFeatures.roadmap is false", () => {
const onChangeView = vi.fn(); 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")); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-roadmaps")).toBeNull(); expect(screen.queryByTestId("view-overflow-roadmaps")).toBeNull();
}); });
it("does not render Skills overflow item when showSkillsTab is false", () => { it("does not render Skills overflow item when showSkillsTab is false", () => {
const onChangeView = vi.fn(); 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")); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-overflow-skills")).toBeNull(); expect(screen.queryByTestId("view-overflow-skills")).toBeNull();
}); });
it("does not render memory overflow item when memoryView is not enabled", () => { it("does not render memory overflow item when memoryView is not enabled", () => {
const onChangeView = vi.fn(); 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")); fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.queryByTestId("view-toggle-memory")).toBeNull(); expect(screen.queryByTestId("view-toggle-memory")).toBeNull();
}); });