feat(FN-2027): gate Insights and Roadmaps views behind experimental flags
- Add insightsEnabled and roadmapEnabled state in useAppSettings derived from project experimentalFeatures. - Pass experimental feature visibility into Header and MobileNavBar and hide Insights/Roadmaps navigation controls unless enabled. - Redirect users back to Board when a disabled Insights or Roadmaps view is active after settings load. - Update Settings modal to always show labeled Insights and Roadmaps feature toggles alongside any custom experimental flags. - Expand App, Header, MobileNavBar, and SettingsModal tests to cover gated rendering and behavior.
This commit is contained in:
@@ -190,6 +190,8 @@ function AppInner() {
|
||||
showQuickChatFAB,
|
||||
githubTokenConfigured,
|
||||
experimentalFeatures,
|
||||
insightsEnabled,
|
||||
roadmapEnabled,
|
||||
toggleAutoMerge,
|
||||
toggleGlobalPause,
|
||||
toggleEnginePause,
|
||||
@@ -199,6 +201,18 @@ function AppInner() {
|
||||
const skillsEnabled = experimentalFeatures.skillsView === true;
|
||||
const nodesEnabled = experimentalFeatures.nodesView === true;
|
||||
|
||||
// Redirect to board if insights/roadmaps view is disabled
|
||||
// Only run after settings have been loaded (experimentalFeatures is non-empty)
|
||||
useEffect(() => {
|
||||
if (Object.keys(experimentalFeatures).length === 0) return;
|
||||
if (taskView === "insights" && !insightsEnabled) {
|
||||
handleChangeTaskView("board");
|
||||
}
|
||||
if (taskView === "roadmaps" && !roadmapEnabled) {
|
||||
handleChangeTaskView("board");
|
||||
}
|
||||
}, [taskView, insightsEnabled, roadmapEnabled, experimentalFeatures, handleChangeTaskView]);
|
||||
|
||||
// Auto-close nodes overlay if feature flag is toggled off while overlay is open
|
||||
useEffect(() => {
|
||||
if (nodesOpen && !nodesEnabled) {
|
||||
@@ -589,6 +603,7 @@ function AppInner() {
|
||||
}
|
||||
}}
|
||||
isRemote={isRemote}
|
||||
experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled }}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && !nodesOpen && taskView !== "missions" && !modalManager.isPlanningOpen && (
|
||||
<SessionNotificationBanner
|
||||
@@ -650,6 +665,7 @@ function AppInner() {
|
||||
onOpenQuickChat={() => setQuickChatOpen(true)}
|
||||
projectId={currentProject?.id}
|
||||
showSkillsTab={skillsEnabled}
|
||||
experimentalFeatures={{ insights: insightsEnabled, roadmap: roadmapEnabled }}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && taskView !== "chat" && taskView !== "mailbox" && taskView !== "insights" && (
|
||||
<QuickChatFAB
|
||||
|
||||
@@ -206,6 +206,8 @@ export interface HeaderProps {
|
||||
onSelectNode?: (node: NodeConfig | null) => void;
|
||||
/** Whether the current view is a remote node */
|
||||
isRemote?: boolean;
|
||||
/** Experimental feature flags controlling visibility of nav items. */
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean };
|
||||
}
|
||||
|
||||
export function Header({
|
||||
@@ -248,6 +250,7 @@ export function Header({
|
||||
currentNode,
|
||||
onSelectNode,
|
||||
isRemote = false,
|
||||
experimentalFeatures,
|
||||
}: HeaderProps) {
|
||||
const mode: ViewportMode = useViewportMode();
|
||||
const isMobile = mode === "mobile";
|
||||
@@ -747,24 +750,28 @@ export function Header({
|
||||
<Zap size={16} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`view-toggle-btn${view === "roadmaps" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("roadmaps")}
|
||||
title="Roadmaps view"
|
||||
aria-label="Roadmaps view"
|
||||
aria-pressed={view === "roadmaps"}
|
||||
>
|
||||
<Map size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={`view-toggle-btn${view === "insights" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("insights")}
|
||||
title="Insights view"
|
||||
aria-label="Insights view"
|
||||
aria-pressed={view === "insights"}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
className={`view-toggle-btn${view === "roadmaps" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("roadmaps")}
|
||||
title="Roadmaps view"
|
||||
aria-label="Roadmaps view"
|
||||
aria-pressed={view === "roadmaps"}
|
||||
>
|
||||
<Map size={16} />
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.insights && (
|
||||
<button
|
||||
className={`view-toggle-btn${view === "insights" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("insights")}
|
||||
title="Insights view"
|
||||
aria-label="Insights view"
|
||||
aria-pressed={view === "insights"}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface MobileNavBarProps {
|
||||
onViewAllProjects?: () => void;
|
||||
/** Whether to show the skills tab */
|
||||
showSkillsTab?: boolean;
|
||||
/** Experimental feature flags controlling visibility of nav items. */
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean };
|
||||
}
|
||||
|
||||
function GitHubLogo({ size = 20 }: { size?: number }) {
|
||||
@@ -103,6 +105,7 @@ export function MobileNavBar({
|
||||
projectId,
|
||||
onViewAllProjects,
|
||||
showSkillsTab,
|
||||
experimentalFeatures,
|
||||
}: MobileNavBarProps) {
|
||||
const mode = useViewportMode();
|
||||
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
||||
@@ -258,17 +261,19 @@ export function MobileNavBar({
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-nav-tab${view === "roadmaps" ? " mobile-nav-tab--active" : ""}`}
|
||||
data-testid="mobile-nav-tab-roadmaps"
|
||||
role="tab"
|
||||
aria-selected={view === "roadmaps"}
|
||||
onClick={() => onChangeView("roadmaps")}
|
||||
>
|
||||
<Map />
|
||||
<span className="mobile-nav-tab-label">Roadmaps</span>
|
||||
</button>
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
type="button"
|
||||
className={`mobile-nav-tab${view === "roadmaps" ? " mobile-nav-tab--active" : ""}`}
|
||||
data-testid="mobile-nav-tab-roadmaps"
|
||||
role="tab"
|
||||
aria-selected={view === "roadmaps"}
|
||||
onClick={() => onChangeView("roadmaps")}
|
||||
>
|
||||
<Map />
|
||||
<span className="mobile-nav-tab-label">Roadmaps</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -505,25 +510,29 @@ export function MobileNavBar({
|
||||
<span>Documents</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-roadmaps"
|
||||
onClick={() => handleMoreAction(() => onChangeView("roadmaps"))}
|
||||
>
|
||||
<Map />
|
||||
<span>Roadmaps</span>
|
||||
</button>
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-roadmaps"
|
||||
onClick={() => handleMoreAction(() => onChangeView("roadmaps"))}
|
||||
>
|
||||
<Map />
|
||||
<span>Roadmaps</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-insights"
|
||||
onClick={() => handleMoreAction(() => onChangeView("insights"))}
|
||||
>
|
||||
<Sparkles />
|
||||
<span>Insights</span>
|
||||
</button>
|
||||
{experimentalFeatures?.insights && (
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-insights"
|
||||
onClick={() => handleMoreAction(() => onChangeView("insights"))}
|
||||
>
|
||||
<Sparkles />
|
||||
<span>Insights</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="mobile-more-separator" />
|
||||
|
||||
|
||||
@@ -644,7 +644,7 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getByText("Experimental Features")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows empty state message when no experimental features are configured", async () => {
|
||||
it("shows known experimental features (Insights, Roadmaps) even when no custom features are configured", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -653,7 +653,9 @@ describe("SettingsModal", () => {
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
expect(screen.getByText(/No experimental features configured/i)).toBeInTheDocument();
|
||||
// Known features should always be shown
|
||||
expect(screen.getByText("Insights")).toBeInTheDocument();
|
||||
expect(screen.getByText("Roadmaps")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows feature flags when experimentalFeatures is set", async () => {
|
||||
@@ -774,7 +776,7 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getByText(/only affect this project/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles undefined experimentalFeatures (falls back to empty)", async () => {
|
||||
it("handles undefined experimentalFeatures (falls back to empty) but still shows known features", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: undefined,
|
||||
@@ -788,8 +790,9 @@ describe("SettingsModal", () => {
|
||||
|
||||
await userEvent.click(screen.getByText("Experimental Features"));
|
||||
|
||||
// Should show empty state since undefined falls back to {}
|
||||
expect(screen.getByText(/No experimental features configured/i)).toBeInTheDocument();
|
||||
// Known features should always be shown regardless of settings
|
||||
expect(screen.getByText("Insights")).toBeInTheDocument();
|
||||
expect(screen.getByText("Roadmaps")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves experimentalFeatures with multiple toggled flags", async () => {
|
||||
|
||||
@@ -77,6 +77,14 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2;
|
||||
|
||||
/** Well-known experimental feature flags with display labels.
|
||||
* These always appear in the Experimental Features settings tab,
|
||||
* regardless of whether they exist in the project's settings blob. */
|
||||
const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
|
||||
insights: "Insights",
|
||||
roadmap: "Roadmaps",
|
||||
};
|
||||
|
||||
export type SectionId = SettingsSection["id"];
|
||||
|
||||
interface SettingsModalProps {
|
||||
@@ -2522,7 +2530,11 @@ export function SettingsModal({
|
||||
}
|
||||
case "experimental": {
|
||||
const experimentalFeatures = form.experimentalFeatures ?? {};
|
||||
const featureFlags = Object.entries(experimentalFeatures).sort(([a], [b]) => a.localeCompare(b));
|
||||
// Merge known features (always shown) with any custom features from settings
|
||||
const allFeatureKeys = Array.from(
|
||||
new Set([...Object.keys(KNOWN_EXPERIMENTAL_FEATURES), ...Object.keys(experimentalFeatures)])
|
||||
).sort((a, b) => a.localeCompare(b));
|
||||
const featureFlags = allFeatureKeys.map((key) => [key, experimentalFeatures[key] === true] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -2535,38 +2547,30 @@ export function SettingsModal({
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{featureFlags.length === 0 ? (
|
||||
<div className="form-group">
|
||||
<small className="settings-muted">
|
||||
No experimental features configured. Features will appear here once added by the system.
|
||||
</small>
|
||||
<div className="form-group">
|
||||
<label>Feature Flags</label>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||
{featureFlags.map(([key, enabled]) => (
|
||||
<label key={key} htmlFor={`experimental-${key}`} className="checkbox-label">
|
||||
<input
|
||||
id={`experimental-${key}`}
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => {
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
experimentalFeatures: {
|
||||
...(f.experimentalFeatures ?? {}),
|
||||
[key]: e.target.checked,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<span>{KNOWN_EXPERIMENTAL_FEATURES[key] ?? key}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label>Feature Flags</label>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||
{featureFlags.map(([key, enabled]) => (
|
||||
<label key={key} htmlFor={`experimental-${key}`} className="checkbox-label">
|
||||
<input
|
||||
id={`experimental-${key}`}
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) => {
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
experimentalFeatures: {
|
||||
...(f.experimentalFeatures ?? {}),
|
||||
[key]: e.target.checked,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<span>{key}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const defaultSettings: Settings = {
|
||||
worktreeInitCommand: "",
|
||||
testCommand: "",
|
||||
buildCommand: "",
|
||||
experimentalFeatures: { insights: true, roadmap: true },
|
||||
};
|
||||
|
||||
vi.mock("../../api", async (importOriginal) => {
|
||||
@@ -1306,6 +1307,24 @@ describe("App view switching", () => {
|
||||
localStorage.removeItem("kb:proj_a:kb-dashboard-task-view");
|
||||
localStorage.removeItem("kb:proj_b:kb-dashboard-task-view");
|
||||
});
|
||||
|
||||
it("does not render insights view button when insights experimental feature is disabled", async () => {
|
||||
// Override fetchSettings to return insights as disabled
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { insights: false },
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the header to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Board view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Insights button should not be rendered
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("App GitHub import", () => {
|
||||
|
||||
@@ -453,16 +453,16 @@ describe("Header", () => {
|
||||
|
||||
// ── Roadmaps View Toggle ───────────────────────────────────────
|
||||
|
||||
it("renders roadmaps view button in view toggle when onChangeView is provided", () => {
|
||||
it("renders roadmaps view button in view toggle when experimentalFeatures.roadmap is true", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
expect(roadmapsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'roadmaps' when roadmaps view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
fireEvent.click(roadmapsBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("roadmaps");
|
||||
@@ -470,7 +470,7 @@ describe("Header", () => {
|
||||
|
||||
it("marks roadmaps view button as active when view is 'roadmaps'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="roadmaps" onChangeView={onChangeView} />);
|
||||
render(<Header view="roadmaps" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
expect(roadmapsBtn.className).toContain("active");
|
||||
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
@@ -478,24 +478,36 @@ describe("Header", () => {
|
||||
|
||||
it("does not mark roadmaps view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
expect(roadmapsBtn.className).not.toContain("active");
|
||||
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
// ── Insights View Toggle ─────────────────────────────────────────
|
||||
it("does not render roadmaps view button when experimentalFeatures.roadmap is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: false }} />);
|
||||
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders insights view button in view toggle when onChangeView is provided", () => {
|
||||
it("does not render roadmaps view button when experimentalFeatures is not provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
|
||||
});
|
||||
|
||||
// ── Insights View Toggle ─────────────────────────────────────────
|
||||
|
||||
it("renders insights view button in view toggle when experimentalFeatures.insights is true", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'insights' when insights view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
fireEvent.click(insightsBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("insights");
|
||||
@@ -503,7 +515,7 @@ describe("Header", () => {
|
||||
|
||||
it("marks insights view button as active when view is 'insights'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="insights" onChangeView={onChangeView} />);
|
||||
render(<Header view="insights" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn.className).toContain("active");
|
||||
expect(insightsBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
@@ -511,12 +523,24 @@ describe("Header", () => {
|
||||
|
||||
it("does not mark insights view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn.className).not.toContain("active");
|
||||
expect(insightsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
it("does not render insights view button when experimentalFeatures.insights is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: false }} />);
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render insights view button when experimentalFeatures is not provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
});
|
||||
|
||||
// ── Search Visibility by View ─────────────────────────────────────
|
||||
|
||||
it("shows search toggle when view is 'board' on desktop", () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("MobileNavBar", () => {
|
||||
mockViewport("mobile");
|
||||
});
|
||||
|
||||
it("renders eight tab buttons (tasks + agents + missions + chat + mailbox + skills + roadmaps + more) when showSkillsTab is true", () => {
|
||||
it("renders seven tab buttons (tasks + agents + missions + chat + mailbox + skills + more) when showSkillsTab is true", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} showSkillsTab={true} />);
|
||||
|
||||
expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeDefined();
|
||||
@@ -65,10 +65,15 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-nav-tab-chat")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-mailbox")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-skills")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-roadmaps")).toBeDefined();
|
||||
expect(screen.queryByTestId("mobile-nav-tab-roadmaps")).toBeNull();
|
||||
expect(screen.getByTestId("mobile-nav-tab-more")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders roadmaps tab when experimentalFeatures.roadmap is true", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{ roadmap: true }} />);
|
||||
expect(screen.getByTestId("mobile-nav-tab-roadmaps")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not render skills tab when showSkillsTab is false", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} showSkillsTab={false} />);
|
||||
expect(screen.queryByTestId("mobile-nav-tab-skills")).toBeNull();
|
||||
@@ -206,14 +211,26 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-more-item-usage")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-projects")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-chat")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-roadmaps")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-insights")).toBeDefined();
|
||||
expect(screen.queryByTestId("mobile-more-item-roadmaps")).toBeNull();
|
||||
expect(screen.queryByTestId("mobile-more-item-insights")).toBeNull();
|
||||
expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows roadmaps in more sheet when experimentalFeatures.roadmap is true", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{ roadmap: true }} />);
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
expect(screen.getByTestId("mobile-more-item-roadmaps")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows insights in more sheet when experimentalFeatures.insights is true", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{ insights: true }} />);
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
expect(screen.getByTestId("mobile-more-item-insights")).toBeDefined();
|
||||
});
|
||||
|
||||
it("insights item in more sheet calls onChangeView with 'insights'", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
const { container } = render(<MobileNavBar {...props} experimentalFeatures={{ insights: true }} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
fireEvent.click(screen.getByTestId("mobile-more-item-insights"));
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface UseAppSettingsResult {
|
||||
showQuickChatFAB: boolean;
|
||||
githubTokenConfigured: boolean;
|
||||
experimentalFeatures: Record<string, boolean>;
|
||||
insightsEnabled: boolean;
|
||||
roadmapEnabled: boolean;
|
||||
toggleAutoMerge: () => Promise<void>;
|
||||
toggleGlobalPause: () => Promise<void>;
|
||||
toggleEnginePause: () => Promise<void>;
|
||||
@@ -35,6 +37,8 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [showQuickChatFAB, setShowQuickChatFAB] = useState(false);
|
||||
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
|
||||
const [experimentalFeatures, setExperimentalFeatures] = useState<Record<string, boolean>>({});
|
||||
const [insightsEnabled, setInsightsEnabled] = useState(false);
|
||||
const [roadmapEnabled, setRoadmapEnabled] = useState(false);
|
||||
|
||||
/**
|
||||
* Fetches config and settings from the backend and updates local state.
|
||||
@@ -60,6 +64,9 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
setTaskStuckTimeoutMs(settings.taskStuckTimeoutMs);
|
||||
setShowQuickChatFAB(settings.showQuickChatFAB === true);
|
||||
setExperimentalFeatures(settings.experimentalFeatures ?? {});
|
||||
const features = settings.experimentalFeatures ?? {};
|
||||
setInsightsEnabled(features.insights === true);
|
||||
setRoadmapEnabled(features.roadmap === true);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
@@ -121,6 +128,8 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
showQuickChatFAB,
|
||||
githubTokenConfigured,
|
||||
experimentalFeatures,
|
||||
insightsEnabled,
|
||||
roadmapEnabled,
|
||||
toggleAutoMerge,
|
||||
toggleGlobalPause,
|
||||
toggleEnginePause,
|
||||
|
||||
Reference in New Issue
Block a user