import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import path from "path";
vi.mock("../registry", () => ({
getDocsMode: () => "generated",
}));
import {
buildFrameworkNav,
buildFrameworkOnlyNav,
CONTENT_DIR,
inlineSnippets,
loadDoc,
SNIPPET_MAP,
SNIPPETS_DIR,
} from "../docs-render";
import type { NavNode } from "../docs-render";
let tempDir = "";
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(SNIPPETS_DIR, "__pdx-208-"));
});
afterEach(() => {
delete SNIPPET_MAP.Pdx208Parent;
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
tempDir = "";
vi.restoreAllMocks();
});
function writeSnippet(filename: string, body: string): string {
const filePath = path.join(tempDir, filename);
fs.writeFileSync(filePath, body);
return path.relative(SNIPPETS_DIR, filePath);
}
function hasSectionPage(navTree: NavNode[], section: string, page: string) {
let inSection = false;
for (const node of navTree) {
if (node.type === "section") {
inSection = node.title === section;
continue;
}
if (inSection && node.type === "page" && node.title === page) return true;
}
return false;
}
function collectMdxFiles(dir: string): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const filePath = path.join(dir, entry.name);
if (entry.isDirectory()) return collectMdxFiles(filePath);
return entry.isFile() && entry.name.endsWith(".mdx") ? [filePath] : [];
});
}
describe("inlineSnippets", () => {
it("recursively inlines helper components imported from snippets", () => {
const helperRel = writeSnippet("helper.mdx", "Helper body\n");
const parentRel = writeSnippet(
"parent.mdx",
[
`import Pdx208Helper from "@/snippets/${helperRel}";`,
"",
"Before",
"",
"After",
].join("\n"),
);
SNIPPET_MAP.Pdx208Parent = parentRel;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const rendered = inlineSnippets("", "pdx-208");
expect(rendered).toContain("Before");
expect(rendered).toContain("Helper body");
expect(rendered).toContain("After");
expect(rendered).not.toContain("");
expect(warnSpy).not.toHaveBeenCalledWith(
"[docs-render] snippet missing for component",
"Pdx208Helper",
"from slug",
"pdx-208",
);
});
it("preserves non-snippet component imports as runtime components", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const rendered = inlineSnippets(
[
'import RuntimeCard from "@/components/runtime-card";',
"",
"",
].join("\n"),
"pdx-208-runtime",
);
expect(rendered).toContain("");
expect(warnSpy).not.toHaveBeenCalledWith(
"[docs-render] snippet missing for component",
"RuntimeCard",
"from slug",
"pdx-208-runtime",
);
});
it("preserves multiline runtime component imports", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const rendered = inlineSnippets(
[
"import {",
" RuntimeCard,",
'} from "@/components/runtime-card";',
"",
"",
].join("\n"),
"pdx-208-runtime-multiline",
);
expect(rendered).toContain("");
expect(warnSpy).not.toHaveBeenCalledWith(
"[docs-render] snippet missing for component",
"RuntimeCard",
"from slug",
"pdx-208-runtime-multiline",
);
});
});
describe("loadDoc", () => {
it("resolves clean URLs to files stored under route-group folders", () => {
const doc = loadDoc("integrations/aws-strands/telemetry");
expect(doc?.filePath).toContain(
"integrations/aws-strands/(other)/telemetry/index.mdx",
);
});
});
describe("migration docs", () => {
it("recommends CopilotKit from the v2 entrypoint instead of CopilotKitProvider", () => {
const snippet = fs.readFileSync(
path.join(SNIPPETS_DIR, "shared/troubleshooting/migrate-to-v2.mdx"),
"utf8",
);
expect(snippet).toContain(
"The `` provider name — keep using it, but import it from `@copilotkit/react-core/v2`",
);
expect(snippet).toContain(
"Keep the `` provider name, but import it from `@copilotkit/react-core/v2`.",
);
expect(snippet).toContain(
'import { CopilotKit, useAgent } from "@copilotkit/react-core/v2";',
);
expect(snippet).toContain(
'import { CopilotKit, CopilotPopup } from "@copilotkit/react-core/v2";',
);
expect(snippet).not.toContain("CopilotKitProvider");
});
it("keeps v2 reference pages aligned with the CopilotKit v2 entrypoint", () => {
const referenceIndex = fs.readFileSync(
path.join(CONTENT_DIR, "..", "reference/index.mdx"),
"utf8",
);
const componentReference = fs.readFileSync(
path.join(CONTENT_DIR, "..", "reference/components/CopilotKit.mdx"),
"utf8",
);
expect(referenceIndex).toContain(
'import { CopilotKit } from "@copilotkit/react-core/v2";',
);
expect(referenceIndex).not.toContain(
"CopilotKit is imported from the root package",
);
expect(referenceIndex).not.toContain(
"import `CopilotKit` from `@copilotkit/react-core`",
);
expect(componentReference).toContain("`@copilotkit/react-core/v2`");
expect(componentReference).not.toContain("not from the v2 subpackage");
});
it("does not recommend stale v2 package paths in authored docs", () => {
const authoredDocFiles = collectMdxFiles(CONTENT_DIR);
const allowedRootProviderImports = new Set([
path.join(CONTENT_DIR, "migrate/v2.mdx"),
]);
const rootProviderImports = authoredDocFiles.filter((filePath) => {
if (allowedRootProviderImports.has(filePath)) return false;
return fs
.readFileSync(filePath, "utf8")
.includes('import { CopilotKit } from "@copilotkit/react-core";');
});
const oldV2StyleImports = [
...authoredDocFiles,
...collectMdxFiles(SNIPPETS_DIR),
].filter((filePath) =>
fs
.readFileSync(filePath, "utf8")
.includes("@copilotkit/react-ui/v2/styles.css"),
);
expect(rootProviderImports).toEqual([]);
expect(oldV2StyleImports).toEqual([]);
});
});
describe("framework nav", () => {
it("includes the shared React Native platform guide in generated framework nav", () => {
const navTree = buildFrameworkNav(
"langgraph",
"LangGraph (Python)",
"langgraph-python",
);
expect(hasSectionPage(navTree, "Platforms", "React Native")).toBe(true);
});
it("includes the shared React Native platform guide in authored framework nav", () => {
const navTree = buildFrameworkOnlyNav("built-in-agent");
expect(hasSectionPage(navTree, "Platforms", "React Native")).toBe(true);
});
});