import { render, screen } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import { Avatar, Bubble, BubbleList } from "../src/bubble";
describe("Bubble", () => {
test("renders text content", () => {
render(<Bubble text="Hello world" />);
expect(screen.getByText("Hello world")).toBeInTheDocument();
});
test("renders with default size", () => {
render(<Bubble text="Default size" />);
const bubble = screen
.getByText("Default size")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("px-4", "py-2");
});
test("renders with lg size", () => {
render(<Bubble text="Large size" size="lg" />);
const bubble = screen
.getByText("Large size")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("px-6", "py-3", "text-lg");
});
test("renders with sm size", () => {
render(<Bubble text="Small size" size="sm" />);
const bubble = screen
.getByText("Small size")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("px-3", "py-1", "text-sm");
});
test("renders with xs size", () => {
render(<Bubble text="Extra small" size="xs" />);
const bubble = screen
.getByText("Extra small")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("px-2", "py-1", "text-xs");
});
test("renders with md size", () => {
render(<Bubble text="Medium size" size="md" />);
const bubble = screen
.getByText("Medium size")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("px-4", "py-2", "text-base");
});
test("aligns left by default", () => {
render(<Bubble text="Left aligned" />);
const bubble = screen
.getByText("Left aligned")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("self-start");
});
test("aligns right when specified", () => {
render(<Bubble text="Right aligned" align="right" />);
const bubble = screen
.getByText("Right aligned")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("self-end");
});
test("aligns center when specified", () => {
render(<Bubble text="Center aligned" align="center" />);
const bubble = screen
.getByText("Center aligned")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("self-center");
});
test("renders with transparent background", () => {
render(<Bubble text="Transparent" background="transparent" />);
const bubble = screen
.getByText("Transparent")
.closest("[data-slot='bubble']");
expect(bubble).toHaveClass("bg-transparent");
});
test("renders with solid background", () => {
render(<Bubble text="Solid" background="solid" />);
const bubble = screen.getByText("Solid").closest("[data-slot='bubble']");
expect(bubble).toHaveClass("bg-gray-100", "dark:bg-gray-800");
});
test("renders pending state with default indicator", () => {
render(<Bubble text="test" isPending={true} />);
const bubble = document.querySelector("[data-slot='bubble']");
expect(bubble).toBeInTheDocument();
expect(bubble?.querySelector(".animate-bounce")).toBeTruthy();
});
test("renders pending state with custom pending content", () => {
render(
<Bubble text="test" isPending={true} pending={<div>Loading...</div>} />,
);
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
});
describe("Avatar", () => {
test("renders text when no imageUrl", () => {
render(<Avatar text="John" />);
expect(screen.getByText("John")).toBeInTheDocument();
});
test("renders image when imageUrl provided", () => {
render(<Avatar text="Jane" imageUrl="https://example.com/avatar.jpg" />);
const img = screen.getByAltText("Jane");
expect(img).toHaveAttribute("src", "https://example.com/avatar.jpg");
expect(img).toHaveClass("object-cover", "rounded-full");
});
test("renders with default styles", () => {
render(<Avatar text="Test" />);
const avatar = screen.getByText("Test").closest("[data-slot='avatar']");
expect(avatar).toHaveClass(
"w-9",
"h-9",
"rounded-full",
"bg-gray-300",
"dark:bg-gray-800",
);
});
test("renders with custom className", () => {
render(<Avatar text="Custom" className="w-12 h-12" />);
const avatar = screen.getByText("Custom").closest("[data-slot='avatar']");
expect(avatar).toHaveClass("w-12", "h-12");
});
});
describe("BubbleList", () => {
const mockMessages = [
{
id: "1",
role: "user" as const,
parts: [{ type: "text" as const, text: "Hello AI" }],
},
{
id: "2",
role: "assistant" as const,
parts: [{ type: "text" as const, text: "Hello human" }],
},
];
test("renders empty list", () => {
render(<BubbleList messages={[]} />);
const container = document.querySelector("[data-slot='bubble-list']");
expect(container).toBeInTheDocument();
});
test("renders messages", () => {
render(<BubbleList messages={mockMessages} />);
expect(screen.getByText("Hello AI")).toBeInTheDocument();
expect(screen.getByText("Hello human")).toBeInTheDocument();
});
test("renders footer when provided", () => {
render(<BubbleList messages={[]} footer={<div>Footer content</div>} />);
expect(screen.getByText("Footer content")).toBeInTheDocument();
});
test("renders pending indicator when isPending is true", () => {
render(<BubbleList messages={[]} isPending={true} />);
const pendingBubble = document.querySelector("[data-slot='bubble-item']");
expect(pendingBubble).toBeTruthy();
});
test("renders with right-solid background by default", () => {
render(<BubbleList messages={mockMessages} />);
const container = document.querySelector("[data-slot='bubble-list']");
expect(container).toBeInTheDocument();
});
test("renders with assistant avatar", () => {
render(
<BubbleList
messages={mockMessages}
assistant={{ avatar: { text: "AI" } }}
/>,
);
expect(screen.getByText("AI")).toBeInTheDocument();
});
test("renders with assistant avatar from string URL", () => {
render(
<BubbleList
messages={mockMessages}
assistant={{ avatar: { text: "AI Avatar" } }}
/>,
);
const avatarContainer = document.querySelector("[data-slot='avatar']");
expect(avatarContainer?.textContent).toBe("AI Avatar");
});
});