diff --git a/api/src/__tests__/uriComponent.test.ts b/api/src/__tests__/uriComponent.test.ts new file mode 100644 index 0000000..4654139 --- /dev/null +++ b/api/src/__tests__/uriComponent.test.ts @@ -0,0 +1,197 @@ +import { expect } from "chai"; +import { describe, it } from "mocha"; + +describe("encodeURIComponent with slashes", () => { + describe("Basic slash encoding", () => { + it("should encode a single forward slash", () => { + const input = "/"; + const result = encodeURIComponent(input); + expect(result).to.equal("%2F"); + }); + + it("should encode multiple forward slashes", () => { + const input = "///"; + const result = encodeURIComponent(input); + expect(result).to.equal("%2F%2F%2F"); + }); + + it("should encode slashes in a path-like string", () => { + const input = "path/to/resource"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fto%2Fresource"); + }); + + it("should encode slashes with alphanumeric characters", () => { + const input = "user123/folder456/file789"; + const result = encodeURIComponent(input); + expect(result).to.equal("user123%2Ffolder456%2Ffile789"); + }); + }); + + describe("Slashes with special characters", () => { + it("should encode slashes with spaces", () => { + const input = "path with spaces/folder with spaces"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%20with%20spaces%2Ffolder%20with%20spaces"); + }); + + it("should encode slashes with query parameters", () => { + const input = "path/to/resource?param=value"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fto%2Fresource%3Fparam%3Dvalue"); + }); + + it("should encode slashes with ampersands", () => { + const input = "path/to/resource&another"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fto%2Fresource%26another"); + }); + + it("should encode slashes with hash symbols", () => { + const input = "path/to/#section"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fto%2F%23section"); + }); + + it("should encode slashes with equals signs", () => { + const input = "path/to/key=value"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fto%2Fkey%3Dvalue"); + }); + }); + + describe("Edge cases with slashes", () => { + it("should handle leading slash", () => { + const input = "/path/to/resource"; + const result = encodeURIComponent(input); + expect(result).to.equal("%2Fpath%2Fto%2Fresource"); + }); + + it("should handle trailing slash", () => { + const input = "path/to/resource/"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fto%2Fresource%2F"); + }); + + it("should handle both leading and trailing slashes", () => { + const input = "/path/to/resource/"; + const result = encodeURIComponent(input); + expect(result).to.equal("%2Fpath%2Fto%2Fresource%2F"); + }); + + it("should handle consecutive slashes", () => { + const input = "path//to///resource"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2F%2Fto%2F%2F%2Fresource"); + }); + + it("should handle empty string", () => { + const input = ""; + const result = encodeURIComponent(input); + expect(result).to.equal(""); + }); + + it("should handle string with only slashes", () => { + const input = "////"; + const result = encodeURIComponent(input); + expect(result).to.equal("%2F%2F%2F%2F"); + }); + }); + + describe("Real-world scenarios", () => { + it("should encode file paths", () => { + const input = "documents/2024/report.pdf"; + const result = encodeURIComponent(input); + expect(result).to.equal("documents%2F2024%2Freport.pdf"); + }); + + it("should encode URL-like strings", () => { + const input = "https://example.com/path/to/resource"; + const result = encodeURIComponent(input); + expect(result).to.equal("https%3A%2F%2Fexample.com%2Fpath%2Fto%2Fresource"); + }); + + it("should encode user input with slashes", () => { + const input = "user/name/with/slashes"; + const result = encodeURIComponent(input); + expect(result).to.equal("user%2Fname%2Fwith%2Fslashes"); + }); + + it("should encode file path with spaces and slashes", () => { + const input = "My Documents/Project Files/report 2024.pdf"; + const result = encodeURIComponent(input); + expect(result).to.equal("My%20Documents%2FProject%20Files%2Freport%202024.pdf"); + }); + + it("should encode nested folder structure", () => { + const input = "root/subfolder1/subfolder2/subfolder3/file.txt"; + const result = encodeURIComponent(input); + expect(result).to.equal("root%2Fsubfolder1%2Fsubfolder2%2Fsubfolder3%2Ffile.txt"); + }); + }); + + describe("Comparison with other characters", () => { + it("should encode backslashes differently than forward slashes", () => { + const forwardSlash = "/"; + const backslash = "\\"; + expect(encodeURIComponent(forwardSlash)).to.equal("%2F"); + expect(encodeURIComponent(backslash)).to.equal("%5C"); + }); + + it("should not encode unreserved characters", () => { + const input = "abc123-._~"; + const result = encodeURIComponent(input); + expect(result).to.equal("abc123-._~"); + }); + + it("should encode slashes but not alphanumeric characters", () => { + const input = "a/b/c/1/2/3"; + const result = encodeURIComponent(input); + expect(result).to.equal("a%2Fb%2Fc%2F1%2F2%2F3"); + }); + }); + + describe("Unicode characters with slashes", () => { + it("should encode Unicode characters and slashes", () => { + const input = "文档/文件"; + const result = encodeURIComponent(input); + expect(result).to.equal("%E6%96%87%E6%A1%A3%2F%E6%96%87%E4%BB%B6"); + }); + + it("should encode emoji with slashes", () => { + const input = "folder/😀/file"; + const result = encodeURIComponent(input); + expect(result).to.equal("folder%2F%F0%9F%98%80%2Ffile"); + }); + + it("should encode mixed Unicode and ASCII with slashes", () => { + const input = "path/café/über"; + const result = encodeURIComponent(input); + expect(result).to.equal("path%2Fcaf%C3%A9%2F%C3%BCber"); + }); + }); + + describe("Decoding encoded slashes", () => { + it("should correctly decode encoded slashes", () => { + const encoded = "path%2Fto%2Fresource"; + const decoded = decodeURIComponent(encoded); + expect(decoded).to.equal("path/to/resource"); + }); + + it("should correctly encode and decode round-trip", () => { + const original = "path/to/resource/with/slashes"; + const encoded = encodeURIComponent(original); + const decoded = decodeURIComponent(encoded); + expect(decoded).to.equal(original); + }); + + it("should handle multiple encode/decode cycles", () => { + const original = "path/to/resource"; + const encoded1 = encodeURIComponent(original); + const encoded2 = encodeURIComponent(encoded1); + const decoded1 = decodeURIComponent(encoded2); + const decoded2 = decodeURIComponent(decoded1); + expect(decoded2).to.equal(original); + }); + }); +}); diff --git a/api/src/tablo.ts b/api/src/tablo.ts index 1e2a159..b3cac61 100644 --- a/api/src/tablo.ts +++ b/api/src/tablo.ts @@ -418,7 +418,7 @@ tabloRouter.post("/invite", regularUserCheckMiddleware, async (c) => { ${introEmail ? `

${introEmail}

` : ""}

Cliquez sur ce lien pour accepter l'invitation.


@@ -443,7 +443,7 @@ tabloRouter.post("/join", async (c) => { .select("id, tablo_id, invited_by") .eq("invite_token", token) .eq("invited_email", joiner.email) - .single(); + .maybeSingle(); if (error) { console.error("error", error); diff --git a/apps/main/src/components/AnimatedBackground.test.tsx b/apps/main/src/components/AnimatedBackground.test.tsx new file mode 100644 index 0000000..8eb4c7b --- /dev/null +++ b/apps/main/src/components/AnimatedBackground.test.tsx @@ -0,0 +1,29 @@ +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..dfcd822 --- /dev/null +++ b/apps/main/src/components/AvailabilityCard.test.tsx @@ -0,0 +1,122 @@ +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..c9f0be8 --- /dev/null +++ b/apps/main/src/components/AvailabilityVisualization.test.tsx @@ -0,0 +1,67 @@ +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..6f0c421 --- /dev/null +++ b/apps/main/src/components/ChannelBadge.test.tsx @@ -0,0 +1,81 @@ +import { render } 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", + id: "test-id", + user_id: "user-id", + access_level: "admin", + is_admin: true, + created_at: "2024-01-01T00:00:00Z", + deleted_at: "2024-01-01T00:00:00Z", + position: 0, + status: "active", + image: null, + }; + 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", + id: "test-id", + user_id: "user-id", + access_level: "admin", + is_admin: true, + created_at: "2024-01-01T00:00:00Z", + deleted_at: "2024-01-01T00:00:00Z", + position: 0, + status: "active", + image: null, + }; + 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..5a456a0 --- /dev/null +++ b/apps/main/src/components/ChannelPreview.test.tsx @@ -0,0 +1,105 @@ +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: ({ displayTitle, isOnline }: { displayTitle?: string; isOnline: boolean }) => ( +
+ {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", + user_id: "user-id", + access_level: "admin", + is_admin: true, + created_at: "2024-01-01T00:00:00Z", + deleted_at: "2024-01-01T00:00:00Z", + position: 0, + status: "active", + image: null, + }; + + 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..b1ee815 --- /dev/null +++ b/apps/main/src/components/ClickOutside.test.tsx @@ -0,0 +1,55 @@ +import { 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 } as { current: null; callback?: () => void }; + // Store callback for testing + ref.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..ebb5970 --- /dev/null +++ b/apps/main/src/components/CreateTabloModal.test.tsx @@ -0,0 +1,125 @@ +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, + }: { + children: React.ReactNode; + onClickOutside: () => void; + }) => ( +
+ {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..59146b3 --- /dev/null +++ b/apps/main/src/components/CustomChannelHeader.test.tsx @@ -0,0 +1,117 @@ +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 }: { Avatar?: () => React.ReactElement }) => ( +
{Avatar && }
+ ), + useChannelStateContext: () => ({ + channel: { + id: "test-channel", + data: { + config: { + name: "Test Channel", + }, + }, + }, + }), +})); + +// Mock ChannelBadge +vi.mock("./ChannelBadge", () => ({ + ChannelBadge: ({ displayTitle }: { displayTitle?: string }) => ( +
{displayTitle}
+ ), +})); + +describe("CustomChannelHeader", () => { + const mockTablos = [ + { + id: "test-channel", + name: "Test Tablo", + color: "bg-blue-500", + user_id: "user-id", + access_level: "admin", + is_admin: true, + created_at: "2024-01-01T00:00:00Z", + deleted_at: "2024-01-01T00:00:00Z", + position: 0, + status: "active", + image: null, + }, + ]; + + 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..25b1ac9 --- /dev/null +++ b/apps/main/src/components/CustomLoadingOverlay.test.tsx @@ -0,0 +1,33 @@ +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..ca4caa6 --- /dev/null +++ b/apps/main/src/components/CustomModal.test.tsx @@ -0,0 +1,134 @@ +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 }: { open: boolean; children: React.ReactNode }) => + open ?
{children}
: null, + DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + DialogHeader: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DialogTitle: ({ children }: { children: React.ReactNode }) => ( +
{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..d4783a3 --- /dev/null +++ b/apps/main/src/components/DeleteTabloModal.test.tsx @@ -0,0 +1,153 @@ +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 }: { children: React.ReactNode }) =>
{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", + user_id: "user-id", + access_level: "admin", + is_admin: true, + created_at: "2024-01-01T00:00:00Z", + deleted_at: "2024-01-01T00:00:00Z", + position: 0, + status: "active", + image: null, + }; + + 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..1085efc --- /dev/null +++ b/apps/main/src/components/EmbedConfigModal.test.tsx @@ -0,0 +1,170 @@ +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 }: { open: boolean; children: React.ReactNode }) => + open ?
{children}
: null, + DialogContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + DialogHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogTitle: ({ children }: { children: React.ReactNode }) =>

{children}

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

{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..9e7bd0d --- /dev/null +++ b/apps/main/src/components/EventDetailsModal.test.tsx @@ -0,0 +1,154 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import type { EventAndTablo } from "@xtablo/shared/types/events.types"; +import { describe, expect, it, vi } from "vitest"; +import { EventDetailsModal } from "./EventDetailsModal"; + +// Mock CustomModal +vi.mock("./CustomModal", () => ({ + CustomModal: ({ + isOpen, + children, + title, + }: { + isOpen: boolean; + children: React.ReactNode; + title: string; + }) => + 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", + tablo_id: "tablo-1", + tablo_status: "active", + event_id: "event-1", + } as EventAndTablo; + + 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..f7ce678 --- /dev/null +++ b/apps/main/src/components/EventModal.test.tsx @@ -0,0 +1,74 @@ +import { 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 }: { children: React.ReactNode }) => 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..0755669 --- /dev/null +++ b/apps/main/src/components/EventTypeCard.test.tsx @@ -0,0 +1,141 @@ +import { fireEvent, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { EventType } from "../hooks/event-types"; +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 }: { children: React.ReactNode }) => children, +})); + +vi.mock("../lib/env", () => ({ + isDev: false, +})); + +// Mock translations +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("EventTypeCard", () => { + const mockEventType: EventType = { + 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 }, + requiresApproval: false, + description: "Test description", + }; + + 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..91417de --- /dev/null +++ b/apps/main/src/components/EventTypeModal.test.tsx @@ -0,0 +1,198 @@ +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 }: { open: boolean; children: React.ReactNode }) => + open ?
{children}
: null, + DialogContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogTitle: ({ children }: { children: React.ReactNode }) =>

{children}

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