Files
fusion/packages/dashboard/app/components/__tests__/NativeStructurePreview.test.tsx
gsxdsm d940bf3d5c FN-8358: add roadmap-item structure previews
Enable native previews and hosted navigation for roadmap items.
- Extend native structure references and API resolution through the roadmap adapter.
- Register the bundled Roadmaps view across desktop and mobile navigation.
- Cover roadmap preview availability, navigation, and route behavior.

Files changed:
 ...n-8358-roadmap-item-native-structure-preview.md |  7 ++++
 docs/dashboard-guide.md                            |  6 ++--
 packages/core/src/types.ts                         | 24 ++++++++-----
 packages/dashboard/app/App.tsx                     | 24 ++++++++-----
 .../dashboard/app/components/LeftSidebarNav.tsx    | 12 +++----
 .../app/components/NativeStructurePreview.tsx      | 10 ++++--
 .../app/components/__tests__/App.test.tsx          | 13 ++++---
 .../app/components/__tests__/Header.test.tsx       |  5 ++-
 .../components/__tests__/LeftSidebarNav.test.tsx   |  5 ++-
 .../app/components/__tests__/MobileNavBar.test.tsx |  4 +--
 .../__tests__/NativeStructurePreview.test.tsx      | 40 ++++++++++++++++------
 .../__tests__/registerBundledPluginViews.test.tsx  | 24 ++++++++++---
 .../app/plugins/registerBundledPluginViews.ts      | 23 +++++++++++++
 packages/dashboard/src/native-structure-preview.ts | 25 +++++++++++++-
 .../native-structure-preview-routes.test.ts        | 31 +++++++++++++++--
 .../src/routes/register-task-workflow-routes.ts    |  4 +--
 packages/dashboard/vite.config.ts                  |  4 +++
 packages/dashboard/vitest.config.ts                |  4 +++
 plugins/fusion-plugin-roadmap/package.json         |  5 +++
 .../fusion-plugin-roadmap/src/dashboard-view.tsx   |  5 +++
 plugins/fusion-plugin-roadmap/src/index.ts         | 14 ++++++--
 21 files changed, 223 insertions(+), 66 deletions(-)

Fusion-Task-Id: FN-8358
Fusion-Task-Lineage: c369ce87-eef0-4d06-bf23-bced9c4363f9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 15:45:45 -07:00

123 lines
6.2 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { NativeStructurePreviewPayload, NativeStructureRef } from "@fusion/core";
import { fetchNativeStructurePreview } from "../../api";
import { NativeStructurePreview } from "../NativeStructurePreview";
import { loadAllAppCss } from "../../test/cssFixture";
vi.mock("../../api", () => ({ fetchNativeStructurePreview: vi.fn() }));
const fetchPreview = vi.mocked(fetchNativeStructurePreview);
const refs: NativeStructureRef[] = [
{ kind: "mission", id: "M-1" },
{ kind: "milestone", id: "MS-1" },
{ kind: "research-finding", id: "INS-1" },
{ kind: "eval-result", id: "EV-1" },
{ kind: "goal", id: "G-1" },
{ kind: "roadmap-item", id: "RF-1" },
];
function payload(ref: NativeStructureRef): NativeStructurePreviewPayload {
const views = { mission: "missions", milestone: "missions", "research-finding": "insights", "eval-result": "evals", goal: "goals", "roadmap-item": "roadmaps" } as const;
return {
available: true,
kind: ref.kind,
kindLabel: ref.kind === "roadmap-item" ? "Roadmap item" : ref.kind,
title: `${ref.kind} title`,
excerpt: "Compact excerpt",
openTarget: { view: views[ref.kind], id: ref.id },
};
}
describe("NativeStructurePreview", () => {
it.each(refs)("renders pre-resolved %s cards without fetching", (ref) => {
const onOpen = vi.fn();
render(<NativeStructurePreview ref={ref} payload={payload(ref)} onOpen={onOpen} />);
expect(screen.getByTestId("native-structure-preview")).toHaveAttribute("data-kind", ref.kind);
expect(screen.getByText(`${ref.kind} title`)).toBeInTheDocument();
expect(screen.getByRole("button", { name: new RegExp("Open") })).toBeInTheDocument();
expect(fetchPreview).not.toHaveBeenCalled();
});
it.each(refs)("fetches and renders a ref-only %s card", async (ref) => {
const result = payload(ref);
fetchPreview.mockResolvedValueOnce(result);
render(<NativeStructurePreview ref={ref} onOpen={vi.fn()} />);
await waitFor(() => expect(screen.getByTestId("native-structure-preview")).toHaveAttribute("data-kind", ref.kind));
expect(fetchPreview).toHaveBeenCalledWith(ref);
expect(screen.getByText(`${ref.kind} title`)).toBeInTheDocument();
});
it("dispatches consumer navigation without a dead anchor", () => {
const ref = refs[0];
const result = payload(ref);
const onOpen = vi.fn();
const { container } = render(<NativeStructurePreview ref={ref} payload={result} onOpen={onOpen} />);
fireEvent.click(screen.getByRole("button", { name: "Open mission: mission title" }));
expect(onOpen).toHaveBeenCalledWith(ref, result);
expect(container.querySelector('a[href*="/missions/"]')).toBeNull();
});
it.each(["missing", "soft-deleted"] as const)("renders a graceful %s placeholder", (reason) => {
render(<NativeStructurePreview ref={refs[0]} payload={{ available: false, kind: "mission", id: "M-1", reason }} onOpen={vi.fn()} />);
expect(screen.getByTestId("native-structure-preview-unavailable")).toHaveAttribute("data-reason", reason);
expect(screen.getByText("This structure is unavailable.")).toBeInTheDocument();
});
it("does not render a previous ref's fetched payload after a ref change", async () => {
fetchPreview.mockResolvedValueOnce(payload(refs[0]));
const { rerender } = render(<NativeStructurePreview ref={refs[0]} onOpen={vi.fn()} />);
await waitFor(() => expect(screen.getByText("mission title")).toBeInTheDocument());
fetchPreview.mockImplementationOnce(() => new Promise(() => {}));
rerender(<NativeStructurePreview ref={refs[1]} onOpen={vi.fn()} />);
expect(screen.getByTestId("native-structure-preview-loading")).toBeInTheDocument();
expect(screen.queryByText("mission title")).toBeNull();
});
it("renders a deliberate error card when fetching fails", async () => {
fetchPreview.mockRejectedValueOnce(new Error("offline"));
render(<NativeStructurePreview ref={refs[0]} onOpen={vi.fn()} />);
await waitFor(() => expect(screen.getByTestId("native-structure-preview-error")).toBeInTheDocument());
});
it("renders the missing-only roadmap-item placeholder", () => {
const ref: NativeStructureRef = { kind: "roadmap-item", id: "RF-missing" };
render(<NativeStructurePreview ref={ref} payload={{ available: false, kind: "roadmap-item", id: ref.id, reason: "missing" }} onOpen={vi.fn()} />);
expect(screen.getByTestId("native-structure-preview-unavailable")).toHaveAttribute("data-reason", "missing");
expect(screen.getByText("roadmap item")).toBeInTheDocument();
});
it("lets a later caller-supplied payload replace a failed fetch", async () => {
fetchPreview.mockRejectedValueOnce(new Error("offline"));
const { rerender } = render(<NativeStructurePreview ref={refs[0]} onOpen={vi.fn()} />);
await waitFor(() => expect(screen.getByTestId("native-structure-preview-error")).toBeInTheDocument());
rerender(<NativeStructurePreview ref={refs[0]} payload={payload(refs[0])} onOpen={vi.fn()} />);
expect(screen.getByTestId("native-structure-preview")).toBeInTheDocument();
expect(screen.queryByTestId("native-structure-preview-error")).toBeNull();
});
it("dispatches roadmap-item navigation without a dead anchor", () => {
const ref: NativeStructureRef = { kind: "roadmap-item", id: "RF-1" };
const result = payload(ref);
const onOpen = vi.fn();
const { container } = render(<NativeStructurePreview ref={ref} payload={result} onOpen={onOpen} />);
fireEvent.click(screen.getByRole("button", { name: "Open Roadmap item: roadmap-item title" }));
expect(onOpen).toHaveBeenCalledWith(ref, expect.objectContaining({ openTarget: { view: "roadmaps", id: "RF-1" } }));
expect(container.querySelector("a[href]")).toBeNull();
});
it("keeps the roadmap-item open affordance inside the mobile layout contract", () => {
const ref: NativeStructureRef = { kind: "roadmap-item", id: "RF-1" };
const { container } = render(<NativeStructurePreview ref={ref} payload={payload(ref)} onOpen={vi.fn()} />);
expect(container.querySelector(".native-structure-preview__open")).toBeInTheDocument();
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.native-structure-preview__open/);
});
});