Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions packages/ui/src/components/mdx-content/mdx-content.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import type React from "react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { MDXContent } from "./mdx-content";

function Note({ children }: { children: React.ReactNode }) {
return <aside>{children}</aside>;
}

describe("MDXContent", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("renders markdown headings, links, and list items", async () => {
const node = await MDXContent({
content:
"## Getting started\n\nRead the [docs](https://example.com).\n\n- Install\n- Ship",
});

render(node);

expect(
screen.getByRole("heading", { name: "Getting started" }),
).toBeInTheDocument();
expect(screen.getByRole("link", { name: "docs" })).toHaveAttribute(
"href",
"https://example.com",
);
expect(screen.getByText("Install")).toBeInTheDocument();
expect(screen.getByText("Ship")).toBeInTheDocument();
});

it("strips injected component imports before rendering MDX", async () => {
const node = await MDXContent({
components: { Note },
content:
'import { Note } from "./note";\n\n<Note>Imported component content</Note>',
});

render(node);

expect(screen.getByText("Imported component content")).toBeInTheDocument();
expect(document.body).not.toHaveTextContent(
'import { Note } from "./note"',
);
});

it("falls back to markdown when MDX evaluation fails", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => null);
const node = await MDXContent({
content: "## Fallback content\n\n<Broken",
});

render(node);

expect(
screen.getByRole("heading", { name: "Fallback content" }),
).toBeInTheDocument();
expect(consoleError).toHaveBeenCalledWith(
"Error rendering MDX:",
expect.any(Error),
);
});
});
89 changes: 89 additions & 0 deletions packages/ui/src/components/model-selector/model-selector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { fireEvent, render, screen } from "@testing-library/react";
import type React from "react";
import { describe, expect, it, vi } from "vitest";

import { type ModelInfo, ModelSelector } from "./model-selector";

const models: ModelInfo[] = [
{
description: "Fast multimodal model",
id: "openai/gpt-4o",
name: "GPT-4o",
pricing: { input: 2.5, output: 10 },
},
{
description: "Long context reasoning model",
id: "anthropic/claude-3-5-sonnet",
name: "Claude 3.5 Sonnet",
pricing: { input: 3, output: 15 },
},
{
description: "Gemini family model",
id: "google/gemini-1.5-pro",
name: "Gemini 1.5 Pro",
},
];

function getModelItem(name: string): Element {
const item = screen.getByText(name).closest("[cmdk-item]");
if (!item) throw new Error(`Expected ${name} to render as a model item`);
return item;
}

function renderModelSelector(
overrides: Partial<React.ComponentProps<typeof ModelSelector>> = {},
) {
const props = {
models,
onOpenChange: vi.fn(),
onSelectModel: vi.fn(),
open: true,
selectedModelId: "google/gemini-1.5-pro",
...overrides,
};

render(<ModelSelector {...props} />);

return props;
}

describe("ModelSelector", () => {
it("renders the open dialog with selected model promoted and disabled", () => {
renderModelSelector();

expect(screen.getByText("Select Model")).toBeInTheDocument();
expect(screen.getByText("Selected")).toBeInTheDocument();
expect(getModelItem("Gemini 1.5 Pro")).toHaveAttribute(
"aria-disabled",
"true",
);
expect(screen.getByText("In: $2.50/1M")).toBeInTheDocument();
expect(screen.getByText("Out: $10.00/1M")).toBeInTheDocument();
});

it("filters models by search text across provider metadata", () => {
renderModelSelector();

fireEvent.change(
screen.getByPlaceholderText("Search models or providers..."),
{
target: { value: "anthropic" },
},
);

expect(screen.getByText("Claude 3.5 Sonnet")).toBeInTheDocument();
expect(screen.queryByText("GPT-4o")).not.toBeInTheDocument();
expect(screen.queryByText("Gemini 1.5 Pro")).not.toBeInTheDocument();
});

it("selects a non-current model and closes the dialog", () => {
const onOpenChange = vi.fn();
const onSelectModel = vi.fn();
renderModelSelector({ onOpenChange, onSelectModel });

fireEvent.click(getModelItem("Claude 3.5 Sonnet"));

expect(onSelectModel).toHaveBeenCalledWith("anthropic/claude-3-5-sonnet");
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
104 changes: 104 additions & 0 deletions packages/ui/src/components/slideshow/slideshow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { Slideshow, type SlideshowProps } from "./slideshow";

const sections = [
{ id: "intro", title: "Intro" },
{ id: "setup", title: "Setup" },
{ id: "finish", title: "Finish" },
];

function renderSlideshow(overrides: Partial<SlideshowProps> = {}) {
const props = {
completedSections: new Set<string>(),
currentIndex: 0,
onComplete: vi.fn(),
onExit: vi.fn(),
onNavigate: vi.fn(),
onToggleSection: vi.fn(),
renderContent: (section: { title: string }) => (
<article>{section.title} body</article>
),
sections,
title: "Tutorial",
...overrides,
};

const view = render(<Slideshow {...props} />);

return { props, view };
}

function advanceNavigationTimer() {
act(() => {
vi.advanceTimersByTime(150);
});
}

describe("Slideshow", () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
document.body.style.overflow = "";
});

it("renders in a portal and restores body scroll lock on cleanup", () => {
const { view } = renderSlideshow();

expect(screen.getByText("Tutorial")).toBeInTheDocument();
expect(screen.getByText("Intro body")).toBeInTheDocument();
expect(document.body.style.overflow).toBe("hidden");

view.unmount();

expect(document.body.style.overflow).toBe("");
});

it("opens the completion dialog before navigating an incomplete section", () => {
const { props } = renderSlideshow();

fireEvent.click(screen.getByRole("button", { name: /next/i }));

expect(
screen.getByRole("dialog", { name: "Mark section as complete?" }),
).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: /^done/i }));
advanceNavigationTimer();

expect(props.onToggleSection).toHaveBeenCalledWith("intro");
expect(props.onNavigate).toHaveBeenCalledWith(1);
});

it("navigates from the table of contents after the transition delay", () => {
const { props } = renderSlideshow();

fireEvent.click(
screen.getByRole("button", { name: "Open table of contents" }),
);
fireEvent.click(screen.getByRole("button", { name: "Setup" }));
advanceNavigationTimer();

expect(props.onNavigate).toHaveBeenCalledWith(1);
expect(
screen.getByRole("button", { name: "Open table of contents" }),
).toBeInTheDocument();
});

it("handles keyboard exit and next navigation shortcuts", () => {
const { props } = renderSlideshow({
completedSections: new Set(["intro"]),
});

fireEvent.keyDown(document, { key: "ArrowRight" });
advanceNavigationTimer();
expect(props.onNavigate).toHaveBeenCalledWith(1);

fireEvent.keyDown(document, { key: "Escape" });
expect(props.onExit).toHaveBeenCalledTimes(1);
});
});
129 changes: 129 additions & 0 deletions packages/ui/src/components/social-fab/social-fab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { SocialFAB, type SocialFabProps } from "./social-fab";

const originalInnerWidth = window.innerWidth;

const labels = {
close: "Close social actions",
share: "Share",
};

const actions = [
{ id: "share", label: "Share", onClick: vi.fn() },
{ id: "copy", label: "Copy link", onClick: vi.fn() },
];

function setViewportWidth(width: number) {
Object.defineProperty(window, "innerWidth", {
configurable: true,
value: width,
});
act(() => {
window.dispatchEvent(new Event("resize"));
});
}

function renderSocialFab(overrides: Partial<SocialFabProps> = {}) {
return render(
<SocialFAB
actions={actions}
labels={labels}
mainText="More"
{...overrides}
/>,
);
}

function getMainButton(name: string): HTMLElement {
const buttons = screen.getAllByRole("button", { name });
const button = buttons.at(-1);
if (!button) throw new Error(`Expected main button named ${name}`);
return button;
}

function getFirstButton(name: string): HTMLElement {
const button = screen.getAllByRole("button", { name }).at(0);
if (!button) throw new Error(`Expected button named ${name}`);
return button;
}

describe("SocialFAB", () => {
afterEach(() => {
vi.restoreAllMocks();
setViewportWidth(originalInnerWidth);
});

it("renders nothing when hidden", () => {
const { container } = renderSocialFab({ hidden: true });

expect(container.firstChild).toBeNull();
});

it("opens and closes from desktop hover callbacks", () => {
const onClose = vi.fn();
const onOpen = vi.fn();
const { container } = renderSocialFab({ onClose, onOpen });
const wrapper = container.querySelector(".fixed");
if (!wrapper) throw new Error("Expected fixed FAB wrapper");

fireEvent.mouseEnter(wrapper);
expect(
screen.getByRole("button", { name: "Close social actions" }),
).toHaveAttribute("aria-expanded", "true");
expect(onOpen).toHaveBeenCalledWith("hover", "desktop");

fireEvent.mouseLeave(wrapper);
expect(getMainButton("Share")).toHaveAttribute("aria-expanded", "false");
expect(onClose).toHaveBeenCalledWith("hover_leave");
});

it("uses a mobile backdrop to close expanded actions", () => {
setViewportWidth(390);
const onClose = vi.fn();
const onOpen = vi.fn();
renderSocialFab({ onClose, onOpen });

fireEvent.click(getMainButton("Share"));

expect(
screen.getByRole("button", { name: "Close social actions" }),
).toHaveAttribute("aria-expanded", "true");
expect(onOpen).toHaveBeenCalledWith("tap", "mobile");

const backdrop = document.body.querySelector("[aria-hidden='true']");
if (!backdrop) throw new Error("Expected mobile backdrop");
fireEvent.click(backdrop);

expect(getMainButton("Share")).toHaveAttribute("aria-expanded", "false");
expect(onClose).toHaveBeenCalledWith("backdrop");
});

it("opens share platforms on desktop hover and launches selected share URL", () => {
const open = vi.spyOn(window, "open").mockImplementation(() => null);
const onAction = vi.fn();
renderSocialFab({
onAction,
sharePlatforms: [
{
buildUrl: (pageUrl, pageTitle) =>
`https://share.example/?url=${pageUrl}&title=${pageTitle}`,
key: "example",
label: "Example Network",
},
],
});

fireEvent.click(getMainButton("Share"));
fireEvent.mouseEnter(getFirstButton("Share"));
fireEvent.click(screen.getByRole("button", { name: "Example Network" }));

expect(onAction).toHaveBeenCalledWith("share");
expect(open).toHaveBeenCalledWith(
expect.stringContaining("https://share.example/"),
"_blank",
"noopener,noreferrer",
);
});
});
Loading