diff --git a/src/components/ui/Avatar.test.tsx b/src/components/ui/Avatar.test.tsx new file mode 100644 index 0000000..a5cb824 --- /dev/null +++ b/src/components/ui/Avatar.test.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Avatar, AvatarStack } from "./Avatar"; + +// Mock next/image to verify rendered attributes straightforwardly +jest.mock("next/image", () => { + return function DummyImage(props: any) { + // eslint-disable-next-line @next/next/no-img-element + return {props.alt}; + }; +}); + +describe("Avatar component (#278)", () => { + it("constructs dicebear URL with seed when src is not provided", () => { + render(); + const img = screen.getByRole("img", { name: "alice" }); + expect(img).toHaveAttribute("src", "https://api.dicebear.com/9.x/identicon/svg?seed=alice&backgroundType=gradientLinear"); + expect(img).toHaveAttribute("data-unoptimized", "true"); + }); + + it("uses provided src when specified", () => { + render(); + const img = screen.getByRole("img", { name: "alice" }); + expect(img).toHaveAttribute("src", "https://example.com/avatar.png"); + expect(img).toHaveAttribute("data-unoptimized", "false"); + }); +}); + +describe("AvatarStack component (#278)", () => { + it("renders all avatars and no overflow badge when seeds count is less than max", () => { + const seeds = ["alice", "bob", "carol"]; + render(); + + expect(screen.getByRole("img", { name: "alice" })).toBeInTheDocument(); + expect(screen.getByRole("img", { name: "bob" })).toBeInTheDocument(); + expect(screen.getByRole("img", { name: "carol" })).toBeInTheDocument(); + expect(screen.queryByText(/^\+/)).not.toBeInTheDocument(); + }); + + it("renders all avatars and no overflow badge when seeds count equals max", () => { + const seeds = ["a", "b", "c", "d", "e"]; + render(); + + expect(screen.getAllByRole("img")).toHaveLength(5); + expect(screen.queryByText(/^\+/)).not.toBeInTheDocument(); + }); + + it("renders max avatars and exact overflow badge '+N' when seeds count exceeds max", () => { + const seeds = ["a", "b", "c", "d", "e", "f", "g"]; + render(); + + expect(screen.getAllByRole("img")).toHaveLength(5); + const overflow = screen.getByText("+2"); + expect(overflow).toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/Tabs.test.tsx b/src/components/ui/Tabs.test.tsx new file mode 100644 index 0000000..06bcac8 --- /dev/null +++ b/src/components/ui/Tabs.test.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Tabs } from "./Tabs"; + +describe("Tabs component (#277)", () => { + const tabs = [ + { key: "active", label: "Active", count: 3 }, + { key: "completed", label: "Completed", count: 0 }, + { key: "all", label: "All" }, + ]; + + it("renders all tab labels correctly", () => { + render(); + + expect(screen.getByRole("button", { name: /Active/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Completed/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /All/i })).toBeInTheDocument(); + }); + + it("calls onChange with the clicked tab key", () => { + const handleChange = jest.fn(); + render(); + + const completedTab = screen.getByRole("button", { name: /Completed/i }); + fireEvent.click(completedTab); + + expect(handleChange).toHaveBeenCalledTimes(1); + expect(handleChange).toHaveBeenCalledWith("completed"); + }); + + it("applies active styles to the active tab and inactive styles to others", () => { + render(); + + const activeBtn = screen.getByRole("button", { name: /Active/i }); + const completedBtn = screen.getByRole("button", { name: /Completed/i }); + + expect(activeBtn).toHaveClass("bg-white", "text-slate-900"); + expect(completedBtn).toHaveClass("text-slate-500"); + }); + + it("renders count badge only when tab.count is a number (including 0)", () => { + render(); + + expect(screen.getByText("3")).toBeInTheDocument(); + expect(screen.getByText("0")).toBeInTheDocument(); + // 'all' has no count, so only 2 count badges exist + const badges = screen.getAllByText(/^[0-9]+$/); + expect(badges).toHaveLength(2); + }); +});