feat(FN-1709): add InsightsView navigation and routing integration
- Add 'insights' to TaskView type for view state persistence - Add InsightsView routing in App.tsx with scoped persistence - Add Insights nav item to Header with icon and keyboard shortcut - Add Insights nav item to MobileNavBar - Add comprehensive tests for App, Header, and MobileNavBar view switching - Add useViewState tests for scoped persistence
This commit is contained in:
@@ -1215,6 +1215,86 @@ describe("App view switching", () => {
|
||||
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
|
||||
// ── Insights View ──────────────────────────────────────────────────
|
||||
|
||||
it("renders InsightsView when insights view is selected", async () => {
|
||||
render(<App />);
|
||||
|
||||
// Wait for the header to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Insights view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click to switch to insights view
|
||||
fireEvent.click(screen.getByTitle("Insights view"));
|
||||
|
||||
// Insights view should be rendered (it has a insights-view container)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".insights-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Should NOT show board, list, or agents view
|
||||
expect(document.querySelector(".board")).toBeNull();
|
||||
expect(document.querySelector(".list-view")).toBeNull();
|
||||
expect(document.querySelector(".agents-view")).toBeNull();
|
||||
});
|
||||
|
||||
it("persists insights view preference to localStorage", async () => {
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Insights view")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTitle("Insights view"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem(taskViewStorageKey())).toBe("insights");
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes insights view from localStorage if saved", async () => {
|
||||
localStorage.setItem(taskViewStorageKey(), "insights");
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".insights-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getByTitle("Insights view").className).toContain("active");
|
||||
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
|
||||
it("project switch rehydrates each project's own scoped task-view", async () => {
|
||||
const projectA = { id: "proj_a", name: "Project A", path: "/a", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" };
|
||||
const projectB = { id: "proj_b", name: "Project B", path: "/b", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" };
|
||||
|
||||
// Set different views for each project
|
||||
localStorage.setItem("kb:proj_a:kb-dashboard-task-view", "insights");
|
||||
localStorage.setItem("kb:proj_b:kb-dashboard-task-view", "agents");
|
||||
|
||||
mockProjectsState.projects = [projectA, projectB];
|
||||
mockCurrentProjectState.currentProject = projectA;
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for project A's insights view to load
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".insights-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Verify insights is active
|
||||
expect(screen.getByTitle("Insights view").className).toContain("active");
|
||||
|
||||
// Cleanup
|
||||
localStorage.removeItem("kb:proj_a:kb-dashboard-task-view");
|
||||
localStorage.removeItem("kb:proj_b:kb-dashboard-task-view");
|
||||
});
|
||||
});
|
||||
|
||||
describe("App GitHub import", () => {
|
||||
|
||||
@@ -460,6 +460,39 @@ describe("Header", () => {
|
||||
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
// ── Insights View Toggle ─────────────────────────────────────────
|
||||
|
||||
it("renders insights view button in view toggle when onChangeView is provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
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} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
fireEvent.click(insightsBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("insights");
|
||||
});
|
||||
|
||||
it("marks insights view button as active when view is 'insights'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="insights" onChangeView={onChangeView} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn.className).toContain("active");
|
||||
expect(insightsBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
|
||||
it("does not mark insights view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn.className).not.toContain("active");
|
||||
expect(insightsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
// ── 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)", () => {
|
||||
it("renders nine tab buttons (tasks + agents + missions + chat + mailbox + skills + roadmaps + insights + more)", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} />);
|
||||
|
||||
expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeDefined();
|
||||
@@ -66,6 +66,7 @@ describe("MobileNavBar", () => {
|
||||
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.getByTestId("mobile-nav-tab-insights")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-nav-tab-more")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -169,6 +170,26 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-nav-tab-skills").className).not.toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
// ── Insights tab ──────────────────────────────────────────────────
|
||||
|
||||
it("insights tab calls onChangeView with 'insights'", () => {
|
||||
const props = createDefaultProps();
|
||||
render(<MobileNavBar {...props} view="board" />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-insights"));
|
||||
expect(props.onChangeView).toHaveBeenCalledWith("insights");
|
||||
});
|
||||
|
||||
it("insights tab is active when view is 'insights'", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} view="insights" />);
|
||||
expect(screen.getByTestId("mobile-nav-tab-insights").className).toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("insights tab is not active when view is 'board'", () => {
|
||||
render(<MobileNavBar {...createDefaultProps()} view="board" />);
|
||||
expect(screen.getByTestId("mobile-nav-tab-insights").className).not.toContain("mobile-nav-tab--active");
|
||||
});
|
||||
|
||||
it("opens and toggles the more sheet", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
@@ -196,9 +217,22 @@ 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.getByTestId("mobile-more-item-settings")).toBeDefined();
|
||||
});
|
||||
|
||||
it("insights item in more sheet calls onChangeView with 'insights'", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
fireEvent.click(screen.getByTestId("mobile-more-item-insights"));
|
||||
|
||||
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
|
||||
expect(props.onChangeView).toHaveBeenCalledWith("insights");
|
||||
});
|
||||
|
||||
it("activity log item in more sheet calls onOpenActivityLog", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
Reference in New Issue
Block a user