diff --git a/packages/ui/src/components/flow-diagram/flow-diagram.test.tsx b/packages/ui/src/components/flow-diagram/flow-diagram.test.tsx
new file mode 100644
index 00000000..b0d724f6
--- /dev/null
+++ b/packages/ui/src/components/flow-diagram/flow-diagram.test.tsx
@@ -0,0 +1,334 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import type React from "react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { FlowDiagram } from "./flow-diagram";
+import type { FlowDiagramEdge, FlowDiagramNode } from "./types";
+
+type MockFlowNode = {
+ data: {
+ description?: string;
+ label: string;
+ };
+ id: string;
+ position?: {
+ x: number;
+ y: number;
+ };
+};
+
+type MockFlowEdge = {
+ id: string;
+ source: string;
+ target: string;
+};
+
+type MockReactFlowProps = {
+ children?: React.ReactNode;
+ colorMode?: string;
+ edges: MockFlowEdge[];
+ nodes: MockFlowNode[];
+ onNodeClick?: (event: React.MouseEvent, node: MockFlowNode) => void;
+};
+
+const flowRuntime = vi.hoisted(() => {
+ type RuntimeNode = {
+ data: {
+ description?: string;
+ label: string;
+ };
+ id: string;
+ position?: {
+ x: number;
+ y: number;
+ };
+ };
+
+ const currentNodes: RuntimeNode[] = [];
+
+ return {
+ clipboardWrite: vi.fn(() => Promise.resolve()),
+ currentNodes,
+ fetchImage: vi.fn((input: string) =>
+ Promise.resolve(
+ new Response(new Blob([input], { type: "image/png" }), {
+ status: 200,
+ }),
+ ),
+ ),
+ fitView: vi.fn(() => Promise.resolve()),
+ getNodesBounds: vi.fn(() => ({
+ height: 80,
+ width: 120,
+ x: 0,
+ y: 0,
+ })),
+ getViewport: vi.fn(() => ({
+ x: 0,
+ y: 0,
+ zoom: 1,
+ })),
+ getViewportForBounds: vi.fn(() => ({
+ x: 10,
+ y: 20,
+ zoom: 1.25,
+ })),
+ toPng: vi.fn(() => Promise.resolve("data:image/png;base64,diagram")),
+ zoomTo: vi.fn(() => Promise.resolve()),
+ };
+});
+
+vi.mock("next-themes", () => ({
+ useTheme: () => ({ resolvedTheme: "light" }),
+}));
+
+vi.mock("html-to-image", () => ({
+ toPng: flowRuntime.toPng,
+}));
+
+vi.mock("@xyflow/react", () => ({
+ Background: () =>
,
+ BackgroundVariant: { Dots: "dots" },
+ getNodesBounds: flowRuntime.getNodesBounds,
+ getViewportForBounds: flowRuntime.getViewportForBounds,
+ ReactFlow: ({
+ children,
+ colorMode,
+ edges,
+ nodes,
+ onNodeClick,
+ }: MockReactFlowProps) => {
+ const firstNode = nodes[0];
+ const handleNodeClick = (event: React.MouseEvent) => {
+ if (firstNode) onNodeClick?.(event, firstNode);
+ };
+
+ return (
+
+
+ {children}
+
+ );
+ },
+ ReactFlowProvider: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ useReactFlow: () => ({
+ fitView: flowRuntime.fitView,
+ getNodes: () => flowRuntime.currentNodes,
+ getViewport: flowRuntime.getViewport,
+ zoomTo: flowRuntime.zoomTo,
+ }),
+}));
+
+class TestClipboardItem {
+ readonly items: Record;
+
+ constructor(items: Record) {
+ this.items = items;
+ }
+}
+
+const nodes: FlowDiagramNode[] = [
+ {
+ data: { description: "Entry point", label: "Start" },
+ id: "start",
+ position: { x: 0, y: 0 },
+ },
+ {
+ data: { description: "Exit point", label: "End" },
+ id: "end",
+ position: { x: 220, y: 0 },
+ },
+];
+
+const edges: FlowDiagramEdge[] = [
+ { id: "start-end", source: "start", target: "end" },
+];
+
+function setRuntimeNodes(nextNodes: FlowDiagramNode[]) {
+ flowRuntime.currentNodes.splice(
+ 0,
+ flowRuntime.currentNodes.length,
+ ...nextNodes,
+ );
+}
+
+function getReactFlowParent(): HTMLElement {
+ const parent = screen.getByTestId("react-flow").parentElement;
+ if (!parent) throw new Error("Expected ReactFlow parent element");
+ return parent;
+}
+
+describe("FlowDiagram", () => {
+ beforeEach(() => {
+ setRuntimeNodes(nodes);
+ flowRuntime.clipboardWrite.mockReset();
+ flowRuntime.clipboardWrite.mockResolvedValue();
+ flowRuntime.fetchImage.mockClear();
+ flowRuntime.fitView.mockClear();
+ flowRuntime.getNodesBounds.mockClear();
+ flowRuntime.getViewport.mockClear();
+ flowRuntime.getViewportForBounds.mockClear();
+ flowRuntime.toPng.mockClear();
+ flowRuntime.toPng.mockResolvedValue("data:image/png;base64,diagram");
+ flowRuntime.zoomTo.mockClear();
+
+ vi.stubGlobal("ClipboardItem", TestClipboardItem);
+ vi.stubGlobal("fetch", flowRuntime.fetchImage);
+ vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) =>
+ window.setTimeout(() => {
+ callback(0);
+ }, 0),
+ );
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
+ window.clearTimeout(id);
+ });
+ Object.defineProperty(navigator, "clipboard", {
+ configurable: true,
+ value: { write: flowRuntime.clipboardWrite },
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+ document.body.style.overflow = "";
+ });
+
+ it("renders title, canvas sizing, controls, and graph data", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Pipeline")).toBeInTheDocument();
+ expect(screen.getByTestId("react-flow")).toHaveAttribute(
+ "data-node-count",
+ "2",
+ );
+ expect(screen.getByTestId("react-flow")).toHaveAttribute(
+ "data-edge-count",
+ "1",
+ );
+ expect(getReactFlowParent()).toHaveStyle({ height: "320px" });
+ expect(screen.getByLabelText("Zoom in")).toBeInTheDocument();
+ expect(screen.getByLabelText("Zoom out")).toBeInTheDocument();
+ expect(screen.getByLabelText("Fit view")).toBeInTheDocument();
+ expect(screen.getByLabelText("Copy as image")).toBeInTheDocument();
+ expect(screen.getByLabelText("Fullscreen")).toBeInTheDocument();
+ });
+
+ it("hides controls when showControls is false", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("No controls")).toBeInTheDocument();
+ expect(screen.queryByLabelText("Zoom in")).not.toBeInTheDocument();
+ });
+
+ it("warns for invalid flow data", () => {
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => null);
+
+ render(
+ ,
+ );
+
+ expect(warn).toHaveBeenCalledWith(
+ "[FlowDiagram] 1 edge(s) reference non-existent nodes:",
+ ["missing: start -> missing"],
+ );
+ expect(warn).toHaveBeenCalledWith(
+ "[FlowDiagram] 1 node(s) missing position:",
+ ["start"],
+ );
+ });
+
+ it("wires zoom, fit view, and node click handlers", () => {
+ const onNodeClick = vi.fn();
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByLabelText("Zoom in"));
+ fireEvent.click(screen.getByLabelText("Fit view"));
+ fireEvent.click(screen.getByText("Mock first node"));
+
+ expect(flowRuntime.zoomTo).toHaveBeenCalledWith(1.2, { duration: 200 });
+ expect(flowRuntime.fitView).toHaveBeenCalledWith({
+ duration: 200,
+ padding: 0.2,
+ });
+ expect(onNodeClick).toHaveBeenCalledWith(nodes[0]);
+ });
+
+ it("opens fullscreen mode and closes it from the portal control", () => {
+ render();
+
+ fireEvent.click(screen.getByLabelText("Fullscreen"));
+
+ expect(
+ screen.getByRole("dialog", { name: "Flow diagram fullscreen view" }),
+ ).toBeInTheDocument();
+ expect(document.body.style.overflow).toBe("hidden");
+
+ fireEvent.click(screen.getByLabelText("Close fullscreen"));
+
+ expect(
+ screen.queryByRole("dialog", { name: "Flow diagram fullscreen view" }),
+ ).not.toBeInTheDocument();
+ expect(document.body.style.overflow).toBe("");
+ });
+
+ it("copies the rendered flow image to the clipboard", async () => {
+ render();
+
+ fireEvent.click(screen.getByLabelText("Copy as image"));
+
+ await waitFor(() => {
+ expect(flowRuntime.clipboardWrite).toHaveBeenCalledTimes(1);
+ });
+ expect(flowRuntime.getNodesBounds).toHaveBeenCalledWith(nodes);
+ expect(flowRuntime.getViewportForBounds).toHaveBeenCalledWith(
+ { height: 80, width: 120, x: 0, y: 0 },
+ 1024,
+ 768,
+ 0.5,
+ 2,
+ 0.2,
+ );
+ expect(flowRuntime.toPng).toHaveBeenCalledWith(
+ expect.any(HTMLElement),
+ expect.objectContaining({
+ backgroundColor: "white",
+ height: 768,
+ width: 1024,
+ }),
+ );
+ expect(screen.getByLabelText("Copied!")).toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/tutorial-mdx/tutorial-mdx.test.tsx b/packages/ui/src/components/tutorial-mdx/tutorial-mdx.test.tsx
new file mode 100644
index 00000000..3829b925
--- /dev/null
+++ b/packages/ui/src/components/tutorial-mdx/tutorial-mdx.test.tsx
@@ -0,0 +1,84 @@
+import { act, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { mdxComponents, TutorialMDX } from "./tutorial-mdx";
+
+vi.mock("../flow-diagram", () => ({
+ FlowDiagram: ({ title }: { title?: string }) => (
+ {title ?? "Flow diagram"}
+ ),
+}));
+
+describe("TutorialMDX", () => {
+ it("renders plain markdown with styled headings, links, lists, and code", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toHaveClass("tutorial-copy");
+ expect(screen.getByRole("heading", { name: "Intro" })).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "guide" })).toHaveAttribute(
+ "href",
+ "https://example.com",
+ );
+ expect(screen.getByText("pnpm test")).toBeInTheDocument();
+ expect(screen.getByText("Learn")).toBeInTheDocument();
+ expect(screen.getByText("Verify")).toBeInTheDocument();
+ });
+
+ it("does not treat JSX-like text inside fenced code as MDX", () => {
+ const { container } = render(
+ Example\n```"} />,
+ );
+ const codeBlock = container.querySelector("pre");
+ if (!codeBlock) throw new Error("Expected fenced code block");
+
+ expect(codeBlock).toHaveTextContent("Example");
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+ });
+
+ it("renders tutorial MDX components through the component map", async () => {
+ await act(async () => {
+ render(
+ ,
+ );
+ });
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("Heads up");
+ expect(screen.getByText("Remember the flow.")).toBeInTheDocument();
+ });
+
+ it("renders the lazy FlowDiagram mapping from MDX content", async () => {
+ await act(async () => {
+ render(
+ ,
+ );
+ });
+
+ expect(await screen.findByTestId("mock-flow-diagram")).toHaveTextContent(
+ "Architecture map",
+ );
+ });
+
+ it("exports the expected tutorial component mappings", () => {
+ expect(Object.keys(mdxComponents)).toEqual(
+ expect.arrayContaining([
+ "Callout",
+ "FlowDiagram",
+ "Quiz",
+ "Step",
+ "StepByStep",
+ ]),
+ );
+ });
+});