diff --git a/apps/main/src/components/AnimatedBackground.test.tsx b/apps/main/src/components/AnimatedBackground.test.tsx new file mode 100644 index 0000000..bd46a88 --- /dev/null +++ b/apps/main/src/components/AnimatedBackground.test.tsx @@ -0,0 +1,31 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { AnimatedBackground } from "./AnimatedBackground"; + +describe("AnimatedBackground", () => { + it("renders without crashing", () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it("renders multiple animated logo images", () => { + render(); + const images = screen.getAllByAltText("Xtablo"); + expect(images.length).toBeGreaterThan(0); + }); + + it("has pointer-events-none class to prevent interaction", () => { + const { container } = render(); + const wrapper = container.firstChild as HTMLElement; + expect(wrapper).toHaveClass("pointer-events-none"); + }); + + it("has absolute positioning", () => { + const { container } = render(); + const wrapper = container.firstChild as HTMLElement; + expect(wrapper).toHaveClass("absolute"); + expect(wrapper).toHaveClass("inset-0"); + }); +}); + + diff --git a/apps/main/src/components/AvailabilityCard.test.tsx b/apps/main/src/components/AvailabilityCard.test.tsx new file mode 100644 index 0000000..77b35ab --- /dev/null +++ b/apps/main/src/components/AvailabilityCard.test.tsx @@ -0,0 +1,124 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { AvailabilityCard } from "./AvailabilityCard"; + +describe("AvailabilityCard", () => { + const defaultProps = { + day: 0, // Monday + enabled: true, + onEnabledChange: vi.fn(), + timeRanges: [{ start: "09:00", end: "17:00" }], + onTimeRangesChange: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders without crashing", () => { + render(); + expect(screen.getByText("Lundi")).toBeInTheDocument(); + }); + + it("displays the correct day name", () => { + render(); + expect(screen.getByText("Mercredi")).toBeInTheDocument(); + }); + + it("shows enabled state correctly", () => { + render(); + expect(screen.getByText("Disponible")).toBeInTheDocument(); + }); + + it("shows disabled state correctly", () => { + render(); + expect(screen.getByText("Indisponible")).toBeInTheDocument(); + }); + + it("calls onEnabledChange when switch is toggled", () => { + const onEnabledChange = vi.fn(); + render(); + const switchElement = screen.getByRole("switch"); + fireEvent.click(switchElement); + expect(onEnabledChange).toHaveBeenCalled(); + }); + + it("displays time ranges", () => { + render(); + expect(screen.getByDisplayValue("09:00")).toBeInTheDocument(); + expect(screen.getByDisplayValue("17:00")).toBeInTheDocument(); + }); + + it("displays multiple time ranges", () => { + const props = { + ...defaultProps, + timeRanges: [ + { start: "09:00", end: "12:00" }, + { start: "14:00", end: "17:00" }, + ], + }; + render(); + expect(screen.getByDisplayValue("09:00")).toBeInTheDocument(); + expect(screen.getByDisplayValue("12:00")).toBeInTheDocument(); + expect(screen.getByDisplayValue("14:00")).toBeInTheDocument(); + expect(screen.getByDisplayValue("17:00")).toBeInTheDocument(); + }); + + it("shows add button when less than 3 time ranges", () => { + render(); + expect(screen.getByText("Ajouter une plage horaire")).toBeInTheDocument(); + }); + + it("adds time range when add button is clicked", () => { + const onTimeRangesChange = vi.fn(); + render(); + fireEvent.click(screen.getByText("Ajouter une plage horaire")); + expect(onTimeRangesChange).toHaveBeenCalled(); + }); + + it("shows delete button when multiple time ranges", () => { + const props = { + ...defaultProps, + timeRanges: [ + { start: "09:00", end: "12:00" }, + { start: "14:00", end: "17:00" }, + ], + }; + const { container } = render(); + const deleteButtons = container.querySelectorAll("button svg"); + // Should have delete buttons for time ranges + expect(deleteButtons.length).toBeGreaterThan(0); + }); + + it("shows copy button when onCopyToOtherDays is provided", () => { + const props = { + ...defaultProps, + onCopyToOtherDays: vi.fn(), + }; + render(); + expect(screen.getByText("Copier")).toBeInTheDocument(); + }); + + it("calls onCopyToOtherDays when copy button is clicked", () => { + const onCopyToOtherDays = vi.fn(); + const props = { + ...defaultProps, + onCopyToOtherDays, + }; + render(); + fireEvent.click(screen.getByText("Copier")); + expect(onCopyToOtherDays).toHaveBeenCalledWith(0, true, defaultProps.timeRanges); + }); + + it("disables inputs when not enabled", () => { + const props = { + ...defaultProps, + enabled: false, + }; + render(); + const startInput = screen.getByDisplayValue("09:00"); + expect(startInput).toBeDisabled(); + }); +}); + + diff --git a/apps/main/src/components/AvailabilityVisualization.test.tsx b/apps/main/src/components/AvailabilityVisualization.test.tsx new file mode 100644 index 0000000..da77358 --- /dev/null +++ b/apps/main/src/components/AvailabilityVisualization.test.tsx @@ -0,0 +1,69 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { WeeklyAvailability } from "../hooks/availabilities"; +import { AvailabilityVisualization } from "./AvailabilityVisualization"; + +describe("AvailabilityVisualization", () => { + const mockAvailabilities: WeeklyAvailability = { + 0: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] }, + 1: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] }, + 2: { enabled: false, timeRanges: [] }, + 3: { enabled: true, timeRanges: [{ start: "10:00", end: "16:00" }] }, + 4: { enabled: true, timeRanges: [{ start: "09:00", end: "17:00" }] }, + 5: { enabled: false, timeRanges: [] }, + 6: { enabled: false, timeRanges: [] }, + }; + + it("renders without crashing", () => { + render(); + expect(screen.getByText("Heure")).toBeInTheDocument(); + }); + + it("displays all days of the week", () => { + render(); + expect(screen.getByText("Lundi")).toBeInTheDocument(); + expect(screen.getByText("Mardi")).toBeInTheDocument(); + expect(screen.getByText("Mercredi")).toBeInTheDocument(); + expect(screen.getByText("Jeudi")).toBeInTheDocument(); + expect(screen.getByText("Vendredi")).toBeInTheDocument(); + expect(screen.getByText("Samedi")).toBeInTheDocument(); + expect(screen.getByText("Dimanche")).toBeInTheDocument(); + }); + + it("displays time slots", () => { + render(); + // Should show time slots from 6 AM to 11 PM + expect(screen.getByText("09:00")).toBeInTheDocument(); + expect(screen.getByText("12:00")).toBeInTheDocument(); + expect(screen.getByText("17:00")).toBeInTheDocument(); + }); + + it("shows availability grid", () => { + const { container } = render( + + ); + // Check for grid structure + expect(container.querySelector(".grid")).toBeInTheDocument(); + }); + + it("accepts custom slot duration", () => { + render( + + ); + expect(screen.getByText("Heure")).toBeInTheDocument(); + }); + + it("renders calendar structure", () => { + const { container } = render( + + ); + // Check that the calendar has proper structure + const headers = container.querySelectorAll(".grid-cols-8"); + expect(headers.length).toBeGreaterThan(0); + }); +}); + + diff --git a/apps/main/src/components/ChannelBadge.test.tsx b/apps/main/src/components/ChannelBadge.test.tsx new file mode 100644 index 0000000..d36e589 --- /dev/null +++ b/apps/main/src/components/ChannelBadge.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ChannelBadge } from "./ChannelBadge"; + +describe("ChannelBadge", () => { + it("renders without crashing", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeInTheDocument(); + }); + + it("displays initials from tablo name", () => { + const tablo = { name: "Project Alpha", color: "bg-blue-500" } as any; + const { container } = render( + + ); + expect(container).toHaveTextContent("P"); + }); + + it("displays initials from displayTitle when tablo is null", () => { + const { container } = render( + + ); + expect(container).toHaveTextContent("M"); + }); + + it("displays default initial 'C' when no names provided", () => { + const { container } = render( + + ); + expect(container).toHaveTextContent("C"); + }); + + it("shows online indicator when isOnline is true", () => { + const { container } = render(); + const onlineIndicator = container.querySelector(".bg-green-500"); + expect(onlineIndicator).toBeInTheDocument(); + }); + + it("does not show online indicator when isOnline is false", () => { + const { container } = render( + + ); + const onlineIndicator = container.querySelector(".bg-green-500"); + expect(onlineIndicator).not.toBeInTheDocument(); + }); + + it("applies tablo color class when provided", () => { + const tablo = { name: "Test", color: "bg-purple-500" } as any; + const { container } = render( + + ); + const badge = container.querySelector(".bg-purple-500"); + expect(badge).toBeInTheDocument(); + }); +}); + + diff --git a/apps/main/src/components/ChannelPreview.test.tsx b/apps/main/src/components/ChannelPreview.test.tsx new file mode 100644 index 0000000..be35060 --- /dev/null +++ b/apps/main/src/components/ChannelPreview.test.tsx @@ -0,0 +1,99 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { Channel } from "stream-chat"; +import { describe, expect, it, vi } from "vitest"; +import { ChannelPreview } from "./ChannelPreview"; + +// Mock ChannelBadge +vi.mock("./ChannelBadge", () => ({ + ChannelBadge: ({ tablo, displayTitle, isOnline }: any) => ( +
+ {displayTitle}-{isOnline ? "online" : "offline"} +
+ ), +})); + +describe("ChannelPreview", () => { + const mockChannel = { + id: "channel-1", + data: { + created_at: new Date("2024-01-01").toISOString(), + config: { + name: "Test Channel", + }, + }, + state: { + members: {}, + }, + } as unknown as Channel; + + const mockTablo = { + id: "tablo-1", + name: "Test Tablo", + color: "bg-blue-500", + } as any; + + const defaultProps = { + channel: mockChannel, + tablo: mockTablo, + displayTitle: "Test Channel", + }; + + it("renders without crashing", () => { + render(); + expect(screen.getByText("Test Channel")).toBeInTheDocument(); + }); + + it("displays channel title", () => { + render(); + expect(screen.getByText("Test Channel")).toBeInTheDocument(); + }); + + it("renders ChannelBadge component", () => { + render(); + expect(screen.getByTestId("channel-badge")).toBeInTheDocument(); + }); + + it("shows unread count badge when unreadCount > 0", () => { + render(); + expect(screen.getByText("5")).toBeInTheDocument(); + }); + + it("shows 99+ for unread counts over 99", () => { + render(); + expect(screen.getByText("99+")).toBeInTheDocument(); + }); + + it("does not show unread badge when count is 0", () => { + const { container } = render(); + expect(container.querySelector(".min-w-\\[20px\\]")).not.toBeInTheDocument(); + }); + + it("calls setActiveChannel when clicked", () => { + const setActiveChannel = vi.fn(); + render(); + fireEvent.click(screen.getByText("Test Channel")); + expect(setActiveChannel).toHaveBeenCalledWith(mockChannel); + }); + + it("highlights active channel", () => { + const { container } = render(); + expect(container.querySelector(".bg-blue-50")).toBeInTheDocument(); + }); + + it("displays latest message preview", () => { + render(); + expect(screen.getByText("Hello world")).toBeInTheDocument(); + }); + + it("applies custom className", () => { + const { container } = render(); + expect(container.querySelector(".custom-class")).toBeInTheDocument(); + }); + + it("shows active indicator for active channel", () => { + const { container } = render(); + expect(container.querySelector(".bg-blue-500")).toBeInTheDocument(); + }); +}); + + diff --git a/apps/main/src/components/ClickOutside.test.tsx b/apps/main/src/components/ClickOutside.test.tsx new file mode 100644 index 0000000..e13026b --- /dev/null +++ b/apps/main/src/components/ClickOutside.test.tsx @@ -0,0 +1,57 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { ClickOutside } from "./ClickOutside"; + +// Mock the useClickOutside hook +vi.mock("@xtablo/shared/hooks/useClickOutside", () => ({ + useClickOutside: (callback: () => void) => { + const ref = { current: null }; + // Store callback for testing + (ref as any).callback = callback; + return ref; + }, +})); + +describe("ClickOutside", () => { + it("renders without crashing", () => { + const onClickOutside = vi.fn(); + render( + +
Test Content
+
+ ); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); + + it("renders children correctly", () => { + const onClickOutside = vi.fn(); + render( + + + + ); + expect(screen.getByText("Click Me")).toBeInTheDocument(); + }); + + it("applies custom className", () => { + const onClickOutside = vi.fn(); + const { container } = render( + +
Test Content
+
+ ); + expect(container.firstChild).toHaveClass("custom-class"); + }); + + it("renders with disabled prop", () => { + const onClickOutside = vi.fn(); + render( + +
Test Content
+
+ ); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); +}); + + diff --git a/apps/main/src/components/CreateTabloModal.test.tsx b/apps/main/src/components/CreateTabloModal.test.tsx new file mode 100644 index 0000000..bcbdd25 --- /dev/null +++ b/apps/main/src/components/CreateTabloModal.test.tsx @@ -0,0 +1,121 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { CreateTabloModal } from "./CreateTabloModal"; + +// Mock ClickOutside +vi.mock("./ClickOutside", () => ({ + ClickOutside: ({ children, onClickOutside }: any) => ( +
+ {children} +
+ ), +})); + +// Mock translations +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("CreateTabloModal", () => { + const mockOnClose = vi.fn(); + const mockOnCreate = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders without crashing", () => { + render(); + expect(screen.getByText("modals:createTablo.title")).toBeInTheDocument(); + }); + + it("displays name input field", () => { + render(); + expect(screen.getByPlaceholderText("modals:createTablo.namePlaceholder")).toBeInTheDocument(); + }); + + it("allows typing in name input", () => { + render(); + const input = screen.getByPlaceholderText( + "modals:createTablo.namePlaceholder" + ) as HTMLInputElement; + fireEvent.change(input, { target: { value: "New Tablo" } }); + expect(input.value).toBe("New Tablo"); + }); + + it("calls onCreate when create button is clicked with valid name", () => { + render(); + const input = screen.getByPlaceholderText("modals:createTablo.namePlaceholder"); + fireEvent.change(input, { target: { value: "New Tablo" } }); + + const createButton = screen.getByText("common:buttons.create"); + fireEvent.click(createButton); + + expect(mockOnCreate).toHaveBeenCalledWith({ + name: "New Tablo", + status: "todo", + image: null, + color: "bg-blue-500", + }); + }); + + it("does not call onCreate when name is empty", () => { + render(); + const createButton = screen.getByText("common:buttons.create"); + fireEvent.click(createButton); + + expect(mockOnCreate).not.toHaveBeenCalled(); + }); + + it("disables create button when name is empty", () => { + render(); + const createButton = screen.getByText("common:buttons.create"); + expect(createButton).toBeDisabled(); + }); + + it("calls onClose when cancel button is clicked", () => { + render(); + const cancelButton = screen.getByText("common:buttons.cancel"); + fireEvent.click(cancelButton); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it("renders StatusPicker component", () => { + render(); + expect(screen.getByText("À faire")).toBeInTheDocument(); + }); + + it("renders ImageColorPicker component", () => { + render(); + expect(screen.getByText("Style")).toBeInTheDocument(); + }); + + it("resets form after successful creation", () => { + render(); + const input = screen.getByPlaceholderText( + "modals:createTablo.namePlaceholder" + ) as HTMLInputElement; + fireEvent.change(input, { target: { value: "New Tablo" } }); + + const createButton = screen.getByText("common:buttons.create"); + fireEvent.click(createButton); + + expect(input.value).toBe(""); + }); + + it("disables create button when in image mode", () => { + render(); + const input = screen.getByPlaceholderText("modals:createTablo.namePlaceholder"); + fireEvent.change(input, { target: { value: "New Tablo" } }); + + // Switch to image mode + fireEvent.click(screen.getByText("Image (Bientôt disponible)")); + + const createButton = screen.getByText("common:buttons.create"); + expect(createButton).toBeDisabled(); + }); +}); + + diff --git a/apps/main/src/components/CustomChannelHeader.test.tsx b/apps/main/src/components/CustomChannelHeader.test.tsx new file mode 100644 index 0000000..2baac41 --- /dev/null +++ b/apps/main/src/components/CustomChannelHeader.test.tsx @@ -0,0 +1,111 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { CustomChannelHeader } from "./CustomChannelHeader"; + +// Mock stream-chat-react +vi.mock("stream-chat-react", () => ({ + ChannelHeader: ({ Avatar }: any) => ( +
{Avatar && }
+ ), + useChannelStateContext: () => ({ + channel: { + id: "test-channel", + data: { + config: { + name: "Test Channel", + }, + }, + }, + }), +})); + +// Mock ChannelBadge +vi.mock("./ChannelBadge", () => ({ + ChannelBadge: ({ tablo, displayTitle }: any) => ( +
{displayTitle}
+ ), +})); + +describe("CustomChannelHeader", () => { + const mockTablos = [ + { + id: "test-channel", + name: "Test Tablo", + color: "bg-blue-500", + }, + ] as any[]; + + it("renders without crashing", () => { + render(); + expect(screen.getByTestId("channel-header")).toBeInTheDocument(); + }); + + it("renders ChannelHeader component", () => { + render(); + expect(screen.getByTestId("channel-header")).toBeInTheDocument(); + }); + + it("shows toggle button when showToggleButton is true", () => { + render( + + ); + const toggleButton = screen.getByLabelText("Toggle channel list"); + expect(toggleButton).toBeInTheDocument(); + }); + + it("hides toggle button when showToggleButton is false", () => { + render( + + ); + const toggleButton = screen.queryByLabelText("Toggle channel list"); + expect(toggleButton).not.toBeInTheDocument(); + }); + + it("calls onToggleChannelList when toggle button is clicked", () => { + const onToggleChannelList = vi.fn(); + render( + + ); + const toggleButton = screen.getByLabelText("Toggle channel list"); + fireEvent.click(toggleButton); + expect(onToggleChannelList).toHaveBeenCalled(); + }); + + it("applies rotation class when isChannelListExpanded is true", () => { + const { container } = render( + + ); + const svg = container.querySelector(".rotate-180"); + expect(svg).toBeInTheDocument(); + }); + + it("renders without toggle button when onToggleChannelList is not provided", () => { + render(); + const toggleButton = screen.queryByLabelText("Toggle channel list"); + expect(toggleButton).not.toBeInTheDocument(); + }); + + it("renders ChannelBadge with correct props", () => { + render(); + expect(screen.getByTestId("channel-badge")).toBeInTheDocument(); + }); +}); + + diff --git a/apps/main/src/components/CustomLoadingOverlay.test.tsx b/apps/main/src/components/CustomLoadingOverlay.test.tsx new file mode 100644 index 0000000..1651283 --- /dev/null +++ b/apps/main/src/components/CustomLoadingOverlay.test.tsx @@ -0,0 +1,35 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { CustomLoadingOverlay } from "./CustomLoadingOverlay"; + +describe("CustomLoadingOverlay", () => { + it("renders without crashing", () => { + render(); + expect(screen.getByRole("presentation")).toBeInTheDocument(); + }); + + it("displays default loading message", () => { + render(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("displays custom loading message", () => { + render(); + expect(screen.getByText("Please wait...")).toBeInTheDocument(); + }); + + it("displays loading icon", () => { + render(); + const icon = screen.getByAltText("Loading icon"); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveAttribute("src", "/icon.jpg"); + }); + + it("has spinning animation on icon", () => { + render(); + const icon = screen.getByAltText("Loading icon"); + expect(icon).toHaveClass("animate-spin"); + }); +}); + + diff --git a/apps/main/src/components/CustomModal.test.tsx b/apps/main/src/components/CustomModal.test.tsx new file mode 100644 index 0000000..6f9b420 --- /dev/null +++ b/apps/main/src/components/CustomModal.test.tsx @@ -0,0 +1,131 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { CustomModal } from "./CustomModal"; + +// Mock Dialog components from shadcn/ui +vi.mock("@xtablo/ui/components/dialog", () => ({ + Dialog: ({ open, children }: any) => (open ?
{children}
: null), + DialogContent: ({ children, className }: any) => ( +
+ {children} +
+ ), + DialogHeader: ({ children }: any) =>
{children}
, + DialogTitle: ({ children }: any) =>
{children}
, +})); + +describe("CustomModal", () => { + const mockOnClose = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders without crashing when open", () => { + render( + +
Test Content
+
+ ); + expect(screen.getByTestId("dialog")).toBeInTheDocument(); + }); + + it("does not render when closed", () => { + render( + +
Test Content
+
+ ); + expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); + }); + + it("displays the title", () => { + render( + +
Test Content
+
+ ); + expect(screen.getByText("Test Modal Title")).toBeInTheDocument(); + }); + + it("renders children content", () => { + render( + +
Custom Modal Content
+
+ ); + expect(screen.getByText("Custom Modal Content")).toBeInTheDocument(); + }); + + it("applies sm width class", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("max-w-sm"); + }); + + it("applies md width class by default", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("max-w-md"); + }); + + it("applies lg width class", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("max-w-lg"); + }); + + it("applies xl width class", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("max-w-xl"); + }); + + it("applies 2xl width class", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("max-w-2xl"); + }); + + it("applies full width class", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("max-w-full"); + }); + + it("applies auto width class", () => { + render( + +
Test Content
+
+ ); + const content = screen.getByTestId("dialog-content"); + expect(content).toHaveClass("w-auto"); + }); +}); + + diff --git a/apps/main/src/components/DeleteTabloModal.test.tsx b/apps/main/src/components/DeleteTabloModal.test.tsx new file mode 100644 index 0000000..8b4773b --- /dev/null +++ b/apps/main/src/components/DeleteTabloModal.test.tsx @@ -0,0 +1,145 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { DeleteTabloModal } from "./DeleteTabloModal"; + +// Mock ClickOutside +vi.mock("./ClickOutside", () => ({ + ClickOutside: ({ children }: any) =>
{children}
, +})); + +// Mock translations +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("DeleteTabloModal", () => { + const mockTablo = { + id: "tablo-1", + name: "Test Tablo", + color: "bg-blue-500", + } as any; + + const mockOnClose = vi.fn(); + const mockOnConfirm = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders without crashing when tablo is provided", () => { + render( + + ); + expect(screen.getByText("deleteTabloModal.title")).toBeInTheDocument(); + }); + + it("returns null when tablo is null", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + it("displays tablo name in confirmation message", () => { + render( + + ); + expect(screen.getByText(/Test Tablo/)).toBeInTheDocument(); + }); + + it("calls onConfirm when delete button is clicked", () => { + render( + + ); + const deleteButton = screen.getByText("deleteTabloModal.buttons.delete"); + fireEvent.click(deleteButton); + expect(mockOnConfirm).toHaveBeenCalledWith("tablo-1"); + }); + + it("calls onClose when cancel button is clicked", () => { + render( + + ); + const cancelButton = screen.getByText("deleteTabloModal.buttons.cancel"); + fireEvent.click(cancelButton); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it("disables buttons when isDeleting is true", () => { + render( + + ); + const deleteButton = screen.getByText("deleteTabloModal.buttons.deleting"); + const cancelButton = screen.getByText("deleteTabloModal.buttons.cancel"); + expect(deleteButton).toBeDisabled(); + expect(cancelButton).toBeDisabled(); + }); + + it("shows deleting text when isDeleting is true", () => { + render( + + ); + expect(screen.getByText("deleteTabloModal.buttons.deleting")).toBeInTheDocument(); + }); + + it("shows spinner when deleting", () => { + const { container } = render( + + ); + expect(container.querySelector(".animate-spin")).toBeInTheDocument(); + }); + + it("displays warning message", () => { + render( + + ); + expect(screen.getByText("deleteTabloModal.warning")).toBeInTheDocument(); + }); +}); diff --git a/apps/main/src/components/EmbedConfigModal.test.tsx b/apps/main/src/components/EmbedConfigModal.test.tsx new file mode 100644 index 0000000..f4e1c84 --- /dev/null +++ b/apps/main/src/components/EmbedConfigModal.test.tsx @@ -0,0 +1,151 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { EmbedConfigModal } from "./EmbedConfigModal"; + +// Mock Dialog components +vi.mock("@xtablo/ui/components/dialog", () => ({ + Dialog: ({ open, children }: any) => (open ?
{children}
: null), + DialogContent: ({ children }: any) =>
{children}
, + DialogHeader: ({ children }: any) =>
{children}
, + DialogTitle: ({ children }: any) =>

{children}

, + DialogFooter: ({ children }: any) =>
{children}
, +})); + +// Mock other UI components +vi.mock("@xtablo/ui/components/button", () => ({ + Button: ({ children, onClick, variant }: any) => ( + + ), +})); + +vi.mock("@xtablo/ui/components/clipboard", () => ({ + CopyButton: ({ label }: any) => , +})); + +vi.mock("@xtablo/ui/components/label", () => ({ + Label: ({ children }: any) => , +})); + +vi.mock("@xtablo/ui/components/select", () => ({ + Select: ({ children, onValueChange, value }: any) => ( +
onValueChange && onValueChange("embed")} + > + {children} +
+ ), + SelectTrigger: ({ children }: any) =>
{children}
, + SelectValue: () =>
Selected
, + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) =>
{children}
, +})); + +vi.mock("@xtablo/ui/components/typography", () => ({ + TypographyMuted: ({ children }: any) =>
{children}
, + TypographyP: ({ children }: any) =>

{children}

, +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("EmbedConfigModal", () => { + const mockBuildPublicLink = vi.fn((type) => `https://example.com/${type}`); + const mockOnClose = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders when open", () => { + render( + + ); + expect(screen.getByTestId("dialog")).toBeInTheDocument(); + }); + + it("does not render when closed", () => { + render( + + ); + expect(screen.queryByTestId("dialog")).not.toBeInTheDocument(); + }); + + it("displays title", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.title")).toBeInTheDocument(); + }); + + it("displays configuration labels", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.labels.integrationType")).toBeInTheDocument(); + expect(screen.getByText("embedConfigModal.labels.buttonColor")).toBeInTheDocument(); + }); + + it("displays preview link section", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.labels.previewLink")).toBeInTheDocument(); + }); + + it("displays embed code section", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.labels.embedCode")).toBeInTheDocument(); + }); + + it("displays close button", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.buttons.close")).toBeInTheDocument(); + }); + + it("displays preview button", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.buttons.preview")).toBeInTheDocument(); + }); + + it("displays copy button", () => { + render( + + ); + expect(screen.getByText("embedConfigModal.buttons.copy")).toBeInTheDocument(); + }); + + it("calls onClose when close button is clicked", () => { + render( + + ); + fireEvent.click(screen.getByText("embedConfigModal.buttons.close")); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it("calls buildPublicLink to generate URL", () => { + render( + + ); + // buildPublicLink should be called to generate the embed URL + expect(mockBuildPublicLink).toHaveBeenCalled(); + }); +}); + + diff --git a/apps/main/src/components/EventDetailsModal.test.tsx b/apps/main/src/components/EventDetailsModal.test.tsx new file mode 100644 index 0000000..35896b8 --- /dev/null +++ b/apps/main/src/components/EventDetailsModal.test.tsx @@ -0,0 +1,144 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { EventDetailsModal } from "./EventDetailsModal"; + +// Mock CustomModal +vi.mock("./CustomModal", () => ({ + CustomModal: ({ isOpen, children, title }: any) => + isOpen ? ( +
+
{title}
+
{children}
+
+ ) : null, +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: "en" }, + }), +})); + +describe("EventDetailsModal", () => { + const mockEvent = { + id: "event-1", + title: "Test Event", + start_date: "2024-01-15", + start_time: "10:00:00", + end_time: "11:00:00", + description: "Test description", + tablo_name: "Test Tablo", + tablo_color: "bg-blue-500", + } as any; + + const mockOnClose = vi.fn(); + const mockOnEdit = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders without crashing when open", () => { + render(); + expect(screen.getByTestId("custom-modal")).toBeInTheDocument(); + }); + + it("does not render when closed", () => { + render(); + expect(screen.queryByTestId("custom-modal")).not.toBeInTheDocument(); + }); + + it("returns null when event is null", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + it("displays event title", () => { + render(); + expect(screen.getByText("Test Event")).toBeInTheDocument(); + }); + + it("displays event date and time labels", () => { + render(); + expect(screen.getByText("eventDetailsModal.labels.dateTime")).toBeInTheDocument(); + }); + + it("displays tablo information", () => { + render(); + expect(screen.getByText("Test Tablo")).toBeInTheDocument(); + }); + + it("displays description when present", () => { + render(); + expect(screen.getByText("Test description")).toBeInTheDocument(); + }); + + it("shows close button", () => { + render(); + expect(screen.getByText("eventDetailsModal.buttons.close")).toBeInTheDocument(); + }); + + it("calls onClose when close button is clicked", () => { + render(); + fireEvent.click(screen.getByText("eventDetailsModal.buttons.close")); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it("shows edit button when canEdit is true", () => { + render( + + ); + expect(screen.getByText("eventDetailsModal.buttons.edit")).toBeInTheDocument(); + }); + + it("does not show edit button when canEdit is false", () => { + render( + + ); + expect(screen.queryByText("eventDetailsModal.buttons.edit")).not.toBeInTheDocument(); + }); + + it("calls onEdit when edit button is clicked", () => { + render( + + ); + fireEvent.click(screen.getByText("eventDetailsModal.buttons.edit")); + expect(mockOnEdit).toHaveBeenCalled(); + }); + + it("displays status badge", () => { + render(); + // Status badge should be rendered (upcoming, today, or past) + const modal = screen.getByTestId("custom-modal"); + expect(modal).toBeInTheDocument(); + }); + + it("handles event without description", () => { + const eventWithoutDesc = { ...mockEvent, description: null }; + render(); + expect(screen.queryByText("eventDetailsModal.labels.description")).not.toBeInTheDocument(); + }); +}); + + diff --git a/apps/main/src/components/EventModal.test.tsx b/apps/main/src/components/EventModal.test.tsx new file mode 100644 index 0000000..5e3a7ec --- /dev/null +++ b/apps/main/src/components/EventModal.test.tsx @@ -0,0 +1,74 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../utils/testHelpers"; +import { EventModal } from "./EventModal"; + +// Mock hooks and dependencies +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); + return { + ...actual, + useParams: () => ({ event_id: undefined }), + useSearchParams: () => [new URLSearchParams(), vi.fn()], + useNavigate: () => vi.fn(), + }; +}); + +vi.mock("../hooks/events", () => ({ + useEvent: () => ({ data: null }), + useCreateEvents: () => vi.fn(), + useUpdateEvent: () => ({ mutate: vi.fn() }), +})); + +vi.mock("../hooks/tablos", () => ({ + useTablosList: () => ({ + data: [{ id: "tablo-1", name: "Test Tablo" }], + isLoading: false, + }), +})); + +vi.mock("../providers/UserStoreProvider", () => ({ + useUser: () => ({ id: "user-1", name: "Test User" }), + useIsReadOnlyUser: () => false, + TestUserStoreProvider: ({ children }: any) => children, +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: "en" }, + }), +})); + +describe("EventModal", () => { + it("renders in create mode", () => { + renderWithProviders(); + expect(screen.getByText("eventModal.title.create")).toBeInTheDocument(); + }); + + it("renders in edit mode", () => { + renderWithProviders(); + expect(screen.getByText("eventModal.title.edit")).toBeInTheDocument(); + }); + + it("displays form fields", () => { + renderWithProviders(); + expect(screen.getByText("eventModal.labels.title")).toBeInTheDocument(); + expect(screen.getByText("eventModal.labels.tablo")).toBeInTheDocument(); + expect(screen.getByText("eventModal.labels.date")).toBeInTheDocument(); + expect(screen.getByText("eventModal.labels.startTime")).toBeInTheDocument(); + expect(screen.getByText("eventModal.labels.endTime")).toBeInTheDocument(); + expect(screen.getByText("eventModal.labels.description")).toBeInTheDocument(); + }); + + it("displays action buttons", () => { + renderWithProviders(); + expect(screen.getByText("eventModal.buttons.cancel")).toBeInTheDocument(); + expect(screen.getByText("eventModal.buttons.save")).toBeInTheDocument(); + }); + + it("shows edit button text in edit mode", () => { + renderWithProviders(); + expect(screen.getByText("eventModal.buttons.edit")).toBeInTheDocument(); + }); +}); diff --git a/apps/main/src/components/EventTypeCard.test.tsx b/apps/main/src/components/EventTypeCard.test.tsx new file mode 100644 index 0000000..9992dac --- /dev/null +++ b/apps/main/src/components/EventTypeCard.test.tsx @@ -0,0 +1,138 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../utils/testHelpers"; +import { EventTypeCard } from "./EventTypeCard"; + +// Mock hooks +vi.mock("../hooks/event-types", () => ({ + useEventTypes: () => ({ + toggleEventType: vi.fn(), + deleteEventType: vi.fn(), + }), +})); + +vi.mock("../providers/UserStoreProvider", () => ({ + useUser: () => ({ + id: "test-user-id-123", + name: "Test User", + }), + TestUserStoreProvider: ({ children }: any) => children, +})); + +vi.mock("../lib/env", () => ({ + isDev: false, +})); + +// Mock translations +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("EventTypeCard", () => { + const mockEventType = { + id: "1", + name: "30 Min Meeting", + duration: 30, + isActive: true, + standardName: "30-min-meeting", + bufferTime: 10, + maxBookingsPerDay: 5, + minAdvanceBooking: { value: 1, unit: "hours" as const }, + }; + + const handleEditEventType = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders without crashing", () => { + renderWithProviders( + + ); + expect(screen.getByText("30 Min Meeting")).toBeInTheDocument(); + }); + + it("displays event type name", () => { + renderWithProviders( + + ); + expect(screen.getByText("30 Min Meeting")).toBeInTheDocument(); + }); + + it("displays duration information", () => { + renderWithProviders( + + ); + expect(screen.getByText("eventTypeCard.duration")).toBeInTheDocument(); + // Duration is displayed as "30 eventTypeCard.minutes" + const durationElements = screen.getAllByText((content, element) => { + return ( + (element?.textContent?.includes("30") && + element?.textContent?.includes("eventTypeCard.minutes")) || + false + ); + }); + expect(durationElements.length).toBeGreaterThan(0); + }); + + it("displays buffer time when present", () => { + renderWithProviders( + + ); + expect(screen.getByText("eventTypeCard.bufferTime")).toBeInTheDocument(); + expect(screen.getByText(/10/)).toBeInTheDocument(); + }); + + it("displays max bookings per day when present", () => { + renderWithProviders( + + ); + expect(screen.getByText("5")).toBeInTheDocument(); + }); + + it("shows active status when isActive is true", () => { + renderWithProviders( + + ); + expect(screen.getByText("eventTypeCard.active")).toBeInTheDocument(); + }); + + it("shows inactive status when isActive is false", () => { + const inactiveEventType = { ...mockEventType, isActive: false }; + renderWithProviders( + + ); + expect(screen.getByText("eventTypeCard.inactive")).toBeInTheDocument(); + }); + + it("calls handleEditEventType when edit button is clicked", () => { + renderWithProviders( + + ); + const editButton = screen.getByLabelText("eventTypeCard.aria.edit"); + fireEvent.click(editButton); + expect(handleEditEventType).toHaveBeenCalledWith(mockEventType.id, mockEventType); + }); + + it("has settings, preview, edit, and delete buttons", () => { + renderWithProviders( + + ); + expect(screen.getByLabelText("eventTypeCard.aria.settings")).toBeInTheDocument(); + expect(screen.getByLabelText("eventTypeCard.aria.preview")).toBeInTheDocument(); + expect(screen.getByLabelText("eventTypeCard.aria.edit")).toBeInTheDocument(); + expect(screen.getByLabelText("eventTypeCard.aria.delete")).toBeInTheDocument(); + }); + + it("applies opacity when inactive", () => { + const inactiveEventType = { ...mockEventType, isActive: false }; + const { container } = renderWithProviders( + + ); + const card = container.querySelector(".opacity-60"); + expect(card).toBeInTheDocument(); + }); +}); diff --git a/apps/main/src/components/EventTypeModal.test.tsx b/apps/main/src/components/EventTypeModal.test.tsx new file mode 100644 index 0000000..b0074a3 --- /dev/null +++ b/apps/main/src/components/EventTypeModal.test.tsx @@ -0,0 +1,176 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { EventTypeConfig } from "../hooks/event-types"; +import { EventTypeModal } from "./EventTypeModal"; + +// Mock Dialog components +vi.mock("@xtablo/ui/components/dialog", () => ({ + Dialog: ({ open, children }: any) => (open ?
{children}
: null), + DialogContent: ({ children }: any) =>
{children}
, + DialogHeader: ({ children }: any) =>
{children}
, + DialogTitle: ({ children }: any) =>

{children}

, + DialogFooter: ({ children }: any) =>
{children}
, +})); + +// Mock other components +vi.mock("@xtablo/ui/components/button", () => ({ + Button: ({ children, onClick }: any) => , +})); + +vi.mock("@xtablo/ui/components/input", () => ({ + Input: ({ value, onChange, type }: any) => ( + + ), +})); + +vi.mock("@xtablo/ui/components/label", () => ({ + Label: ({ children }: any) => , +})); + +vi.mock("@xtablo/ui/components/textarea", () => ({ + Textarea: ({ value, onChange }: any) =>