diff --git a/packages/ui/components/button/Button.tsx b/packages/ui/components/button/Button.tsx index 013192e941..f6b2aea2b2 100644 --- a/packages/ui/components/button/Button.tsx +++ b/packages/ui/components/button/Button.tsx @@ -35,8 +35,8 @@ export type ButtonProps = ButtonBaseProps & | (Omit & { href?: never }) ); -const buttonClasses = cva( - "inline-flex items-center text-sm font-medium relative rounded-md transition-colors disabled:cursor-not-allowed ", +export const buttonClasses = cva( + "inline-flex items-center text-sm font-medium relative rounded-md transition-colors disabled:cursor-not-allowed", { variants: { variant: { @@ -155,7 +155,7 @@ export const Button = forwardRef - + ) : ( - + ) : ( + {element} ) : ( - {element} + + {element} + ); }); @@ -222,5 +224,9 @@ const Wrapper = ({ children, tooltip }: { tooltip?: string; children: React.Reac return <>{children}; } - return {children}; + return ( + + {children} + + ); }; diff --git a/packages/ui/components/button/button.test.tsx b/packages/ui/components/button/button.test.tsx new file mode 100644 index 0000000000..5cd0ce7f52 --- /dev/null +++ b/packages/ui/components/button/button.test.tsx @@ -0,0 +1,249 @@ +/* eslint-disable playwright/missing-playwright-await */ +import { render, screen, fireEvent } from "@testing-library/react"; +import { useState } from "react"; +import { vi } from "vitest"; + +import { Plus } from "@calcom/ui/components/icon"; + +import { Button, buttonClasses } from "./Button"; + +const observeMock = vi.fn(); + +window.ResizeObserver = vi.fn().mockImplementation(() => ({ + disconnect: vi.fn(), + observe: observeMock, + unobserve: vi.fn(), +})); + +vi.mock("../tooltip", async () => { + const actual = (await vi.importActual("../tooltip")) as any; + const TooltipMock = (props: object) => { + const [open, setOpen] = useState(false); + + return ( + { + setOpen(isOpen); + }} + /> + ); + }; + return { + Tooltip: TooltipMock, + }; +}); + +describe("Tests for Button component", () => { + test("Should apply the icon variant class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ variant: "icon" }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the fab variant class", () => { + render(); + expect(screen.getByText("Test Button")).toHaveClass("hidden md:inline"); + }); + + test("Should apply the secondary color class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ color: "secondary" }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the minimal color class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ color: "minimal" }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the sm size class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ size: "sm" }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the base size class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ size: "base" }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the lg size class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ size: "lg" }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the loading class", () => { + render(); + const buttonClass = screen.getByText("Test Button"); + const buttonComponentClass = buttonClasses({ loading: true }); + const buttonClassArray = buttonClass.className.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .some((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + }); + + test("Should apply the disabled class when disabled prop is true", () => { + render(); + const buttonClass = screen.getByText("Test Button").className; + const expectedClassName = "disabled:cursor-not-allowed"; + expect(buttonClass.includes(expectedClassName)).toBe(true); + }); + + test("Should apply the custom class", () => { + const className = "custom-class"; + render(); + expect(screen.getByText("Test Button")).toHaveClass(className); + }); + + test("Should render as a button by default", () => { + render(); + const button = screen.getByText("Test Button"); + expect(button.tagName).toBe("BUTTON"); + }); + + test("Should render StartIcon and Plus icon if is fab variant", () => { + render( + + ); + expect(screen.getByTestId("start-icon")).toBeInTheDocument(); + expect(screen.getByTestId("plus")).toBeInTheDocument(); + }); + + test("Should render just StartIcon if is not fab variant", () => { + render( + + ); + expect(screen.getByTestId("start-icon")).toBeInTheDocument(); + expect(screen.queryByTestId("plus")).not.toBeInTheDocument(); + }); + + test("Should render EndIcon and Plus icon if is fab variant", () => { + render( + + ); + expect(screen.getByTestId("end-icon")).toBeInTheDocument(); + expect(screen.getByTestId("plus")).toBeInTheDocument(); + }); + + test("Should render just EndIcon if is not fab variant", () => { + render( + + ); + expect(screen.getByTestId("end-icon")).toBeInTheDocument(); + expect(screen.queryByTestId("plus")).not.toBeInTheDocument(); + }); + + test("Should render Link if have href", () => { + render(); + + const buttonElement = screen.getByText("Test Button"); + + expect(buttonElement).toHaveAttribute("href", "/test"); + expect(buttonElement.closest("a")).toBeInTheDocument(); + + test("Should render Wrapper if don't have href", () => { + render(); + expect(screen.queryByTestId("link-component")).not.toBeInTheDocument(); + expect(screen.getByText("Test Button")).toBeInTheDocument(); + }); + + test("Should render Tooltip if exists", () => { + render(); + const tooltip = screen.getByTestId("tooltip"); + expect(tooltip.getAttribute("data-state")).toEqual("closed"); + expect(tooltip.getAttribute("data-state")).toEqual("instant-open"); + expect(observeMock).toBeCalledWith(tooltip); + }); + test("Should not render Tooltip if no exists", () => { + render(); + expect(screen.queryByTestId("tooltip")).not.toBeInTheDocument(); + expect(screen.getByText("Test Button")).toBeInTheDocument(); + }); + + test("Should render as a button with a custom type", () => { + render(); + const button = screen.getByText("Test Button"); + expect(button.tagName).toBe("BUTTON"); + expect(button).toHaveAttribute("type", "submit"); + }); + + test("Should render as an anchor when href prop is provided", () => { + render(); + const button = screen.getByText("Test Button"); + expect(button.tagName).toBe("A"); + expect(button).toHaveAttribute("href", "/path"); + }); + + test("Should call onClick callback when clicked", () => { + const handleClick = vi.fn(); + render(); + const button = screen.getByText("Test Button"); + fireEvent.click(button); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + test("Should render default button correctly", () => { + render(); + const buttonClass = screen.getByText("Default Button").className; + const buttonComponentClass = buttonClasses({ variant: "button", color: "primary", size: "base" }); + const buttonClassArray = buttonClass.split(" "); + const hasMatchingClassNames = buttonComponentClass + .split(" ") + .every((className) => buttonClassArray.includes(className)); + expect(hasMatchingClassNames).toBe(true); + expect(screen.getByText("Default Button")).toHaveAttribute("type", "button"); + }); + + test("Should pass the shallow prop to Link component when href prop is passed", () => { + const href = "https://example.com"; + render(