222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
import React from "react";
|
|
import { render, fireEvent, screen } from "@testing-library/react";
|
|
import Icon from "components/Icon";
|
|
import Button from "./Button";
|
|
|
|
describe("Button component", () => {
|
|
it("renders button with correct text", () => {
|
|
render(<Button>Click me</Button>);
|
|
expect(screen.getByText("Click me")).toBeInTheDocument();
|
|
});
|
|
it("applies correct class names", () => {
|
|
const { container } = render(<Button disabled>Test</Button>);
|
|
expect(container.firstChild).toHaveClass(
|
|
"button button--default button--disabled"
|
|
);
|
|
});
|
|
it("calls onClick when clicked", () => {
|
|
const handleClick = jest.fn();
|
|
render(<Button onClick={handleClick}>Click me</Button>);
|
|
fireEvent.click(screen.getByText("Click me"));
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
it("calls onClick when Enter key is pressed", () => {
|
|
const handleClick = jest.fn();
|
|
render(<Button onClick={handleClick}>Press enter</Button>);
|
|
fireEvent.keyDown(screen.getByText("Press enter"), { key: "Enter" });
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
it("does not call onClick when disabled", () => {
|
|
const handleClick = jest.fn();
|
|
render(
|
|
<Button onClick={handleClick} disabled>
|
|
Disabled
|
|
</Button>
|
|
);
|
|
fireEvent.click(screen.getByText("Disabled"));
|
|
expect(handleClick).not.toHaveBeenCalled();
|
|
});
|
|
it("renders spinner when isLoading is true", () => {
|
|
render(<Button isLoading>Loading</Button>);
|
|
expect(screen.getByText("Loading")).toHaveClass("transparent-text");
|
|
expect(screen.getByTestId("spinner")).toBeInTheDocument();
|
|
});
|
|
it("applies custom className", () => {
|
|
const { container } = render(
|
|
<Button className="custom-class">Custom</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("custom-class");
|
|
});
|
|
it("renders with correct title attribute", () => {
|
|
render(<Button title="Button title">Titled button</Button>);
|
|
expect(screen.getByTitle("Button title")).toBeInTheDocument();
|
|
});
|
|
it("applies the bordered secondary variant class", () => {
|
|
const { container } = render(
|
|
<Button variant="secondary">Secondary</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("button button--secondary");
|
|
});
|
|
it("applies the subdued variant class", () => {
|
|
const { container } = render(<Button variant="subdued">Subdued</Button>);
|
|
expect(container.firstChild).toHaveClass("button button--subdued");
|
|
});
|
|
it("applies the small modifier for a small secondary button", () => {
|
|
const { container } = render(
|
|
<Button variant="secondary" size="small">
|
|
Secondary
|
|
</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("button--secondary__small");
|
|
});
|
|
it("adds the icon-only class on a secondary button with icon and no label", () => {
|
|
const { container } = render(
|
|
<Button variant="secondary" icon="trash" ariaLabel="Delete" />
|
|
);
|
|
expect(container.firstChild).toHaveClass("button--icon-only");
|
|
});
|
|
it("sizes the icon to small on a small icon-only button", () => {
|
|
render(
|
|
<Button
|
|
variant="secondary"
|
|
size="small"
|
|
icon="trash"
|
|
ariaLabel="Delete"
|
|
/>
|
|
);
|
|
// Small icon = 12px per ICON_SIZES.
|
|
expect(
|
|
screen.getByTestId("trash-icon").querySelector("svg")
|
|
).toHaveAttribute("width", "12");
|
|
});
|
|
it("renders icon left of the label by default and applies the with-icon class", () => {
|
|
const { container } = render(
|
|
<Button variant="secondary" icon="plus">
|
|
Add
|
|
</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("button--with-icon");
|
|
expect(container.firstChild).not.toHaveClass("button--icon-only");
|
|
expect(screen.getByTestId("plus-icon")).toBeInTheDocument();
|
|
});
|
|
it("renders icon right of the label when iconPosition is right", () => {
|
|
const { container } = render(
|
|
<Button variant="subdued" icon="chevron-right" iconPosition="right">
|
|
Next
|
|
</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("button--with-icon");
|
|
expect(container.firstChild).not.toHaveClass("button--icon-only");
|
|
const wrapper = container.querySelector(".children-wrapper");
|
|
expect(wrapper?.lastElementChild).toBe(
|
|
screen.getByTestId("chevron-right-icon")
|
|
);
|
|
});
|
|
it("does not render the icon or apply icon classes on variants outside the icon-enabled set", () => {
|
|
const { container } = render(
|
|
<Button variant="pill" icon="plus">
|
|
Add
|
|
</Button>
|
|
);
|
|
expect(container.firstChild).not.toHaveClass("button--with-icon");
|
|
expect(container.firstChild).not.toHaveClass("button--icon-only");
|
|
expect(screen.queryByTestId("plus-icon")).not.toBeInTheDocument();
|
|
});
|
|
it("auto-colors the icon white on white-text variants (default)", () => {
|
|
render(
|
|
<Button variant="default" icon="plus">
|
|
Add
|
|
</Button>
|
|
);
|
|
expect(
|
|
screen
|
|
.getByTestId("plus-icon")
|
|
.querySelector("path")
|
|
?.getAttribute("stroke")
|
|
).toContain("core-fleet-white");
|
|
});
|
|
it("auto-colors the icon white on the alert variant", () => {
|
|
render(
|
|
<Button variant="alert" icon="plus">
|
|
Delete
|
|
</Button>
|
|
);
|
|
expect(
|
|
screen
|
|
.getByTestId("plus-icon")
|
|
.querySelector("path")
|
|
?.getAttribute("stroke")
|
|
).toContain("core-fleet-white");
|
|
});
|
|
it("does not render the icon on the oversized variant (icons not enabled)", () => {
|
|
render(
|
|
<Button variant="oversized" icon="plus">
|
|
Continue
|
|
</Button>
|
|
);
|
|
expect(screen.queryByTestId("plus-icon")).not.toBeInTheDocument();
|
|
});
|
|
it("matches the icon color to the text on secondary/subdued", () => {
|
|
render(
|
|
<Button variant="secondary" icon="plus">
|
|
Add
|
|
</Button>
|
|
);
|
|
expect(
|
|
screen
|
|
.getByTestId("plus-icon")
|
|
.querySelector("path")
|
|
?.getAttribute("stroke")
|
|
).toContain("ui-fleet-black-75");
|
|
});
|
|
it("treats a false child as no label and applies icon-only styling", () => {
|
|
// Callers commonly do `{cond && "Label"}` — a false-y cond means no label.
|
|
const { container } = render(
|
|
<Button variant="secondary" icon="trash" ariaLabel="Delete">
|
|
{false && "Delete"}
|
|
</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("button--icon-only");
|
|
expect(container.firstChild).not.toHaveClass("button--with-icon");
|
|
});
|
|
it("renders aria-controls when ariaControls is provided", () => {
|
|
render(<Button ariaControls="menu-1">Open menu</Button>);
|
|
expect(screen.getByRole("button")).toHaveAttribute(
|
|
"aria-controls",
|
|
"menu-1"
|
|
);
|
|
});
|
|
it("omits aria-controls when ariaControls is undefined", () => {
|
|
render(<Button>Plain</Button>);
|
|
expect(screen.getByRole("button")).not.toHaveAttribute("aria-controls");
|
|
});
|
|
it("warns in dev when an icon-only button has neither ariaLabel nor title", () => {
|
|
const warn = jest
|
|
.spyOn(console, "warn")
|
|
.mockImplementation(() => undefined);
|
|
render(<Button variant="secondary" icon="trash" />);
|
|
expect(warn).toHaveBeenCalledWith(
|
|
expect.stringContaining("no ariaLabel or title")
|
|
);
|
|
warn.mockRestore();
|
|
});
|
|
it("does not warn when an icon-only button has a title but no ariaLabel", () => {
|
|
const warn = jest
|
|
.spyOn(console, "warn")
|
|
.mockImplementation(() => undefined);
|
|
render(<Button variant="secondary" icon="trash" title="Delete" />);
|
|
expect(warn).not.toHaveBeenCalled();
|
|
warn.mockRestore();
|
|
});
|
|
it("adds the icon-only class for a legacy <Icon> child pattern on secondary/subdued", () => {
|
|
// Some call sites keep the child <Icon> pattern to pass color/className.
|
|
// We still want the square icon-only styling for those.
|
|
const { container } = render(
|
|
<Button variant="secondary" ariaLabel="Close">
|
|
<Icon name="close" color="core-fleet-black" />
|
|
</Button>
|
|
);
|
|
expect(container.firstChild).toHaveClass("button--icon-only");
|
|
});
|
|
});
|