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>
123 lines
6.2 KiB
TypeScript
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/);
|
|
});
|
|
});
|