mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-14 18:01:06 +00:00
290 lines
9.1 KiB
TypeScript
290 lines
9.1 KiB
TypeScript
import { render, screen } from "@testing-library/svelte";
|
|
import { user } from "../setup-tests";
|
|
import OverflowMenu from "./OverflowMenu.test.svelte";
|
|
|
|
describe("OverflowMenu", () => {
|
|
it("renders and functions correctly", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
expect(menuButton).toHaveAttribute("aria-haspopup", "true");
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "false");
|
|
|
|
await user.click(menuButton);
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "true");
|
|
|
|
const menuItems = screen.getAllByRole("menuitem");
|
|
expect(menuItems).toHaveLength(3);
|
|
expect(menuItems[0]).toHaveFocus();
|
|
|
|
expect(menuItems[0]).toHaveTextContent("Manage credentials");
|
|
expect(menuItems[1]).toHaveTextContent("API documentation");
|
|
expect(menuItems[2]).toHaveTextContent("Delete service");
|
|
|
|
expect(menuItems[1]).toHaveAttribute(
|
|
"href",
|
|
"https://cloud.ibm.com/docs/api-gateway/",
|
|
);
|
|
|
|
expect(menuItems[2].parentElement).toHaveClass(
|
|
"bx--overflow-menu-options__option--danger",
|
|
);
|
|
|
|
const spy = vi.spyOn(console, "log");
|
|
await user.click(menuItems[0]);
|
|
expect(spy).toHaveBeenCalledWith("click", "Manage credentials");
|
|
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "false");
|
|
expect(spy).toHaveBeenCalledWith("close", {
|
|
index: 0,
|
|
text: "Manage credentials",
|
|
});
|
|
});
|
|
|
|
it("handles keyboard navigation", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const spy = vi.spyOn(console, "log");
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "true");
|
|
|
|
let menuItems = screen.getAllByRole("menuitem");
|
|
expect(menuItems[0]).toHaveFocus();
|
|
|
|
await user.keyboard("{ArrowDown}");
|
|
expect(menuItems[1]).toHaveFocus();
|
|
|
|
await user.keyboard("{Enter}");
|
|
expect(spy).toHaveBeenCalledWith("click", "API documentation");
|
|
|
|
await user.click(menuButton);
|
|
menuItems = screen.getAllByRole("menuitem");
|
|
expect(menuItems[0]).toHaveFocus();
|
|
|
|
await user.keyboard("{ArrowUp}");
|
|
expect(menuItems[2]).toHaveFocus();
|
|
|
|
await user.keyboard("{Escape}");
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "false");
|
|
expect(spy).toHaveBeenCalledWith("close", null);
|
|
});
|
|
|
|
it("closes when clicking outside", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "true");
|
|
|
|
await user.click(document.body);
|
|
expect(menuButton).toHaveAttribute("aria-expanded", "false");
|
|
});
|
|
|
|
it("uses CSS custom properties for performance optimization", async () => {
|
|
const { container } = render(OverflowMenu);
|
|
|
|
const styleTags = document.querySelectorAll("style");
|
|
const initialStyleCount = styleTags.length;
|
|
expect(styleTags.length).toBe(initialStyleCount);
|
|
|
|
await user.click(screen.getByRole("button"));
|
|
expect(container.querySelector(".bx--overflow-menu-options")).toHaveStyle(
|
|
"--overflow-menu-options-after-width: 2rem",
|
|
);
|
|
expect(styleTags.length).toBe(initialStyleCount);
|
|
});
|
|
|
|
it("stress test: multiple instances don't create individual style tags", async () => {
|
|
const { container: container1 } = render(OverflowMenu);
|
|
const { container: container2 } = render(OverflowMenu);
|
|
const { container: container3 } = render(OverflowMenu);
|
|
|
|
const styleTagsBefore = document.querySelectorAll("style").length;
|
|
const menuButtons1 = container1.querySelectorAll("button");
|
|
const menuButtons2 = container2.querySelectorAll("button");
|
|
const menuButtons3 = container3.querySelectorAll("button");
|
|
|
|
await user.click(menuButtons1[0]);
|
|
await user.click(menuButtons2[0]);
|
|
await user.click(menuButtons3[0]);
|
|
|
|
const styleTagsAfter = document.querySelectorAll("style").length;
|
|
|
|
// Verify no additional style tags created (old approach would create 3+)
|
|
expect(styleTagsAfter).toBe(styleTagsBefore);
|
|
|
|
// Verify all menus have CSS custom property set.
|
|
const allMenus = [
|
|
...container1.querySelectorAll(".bx--overflow-menu-options"),
|
|
...container2.querySelectorAll(".bx--overflow-menu-options"),
|
|
...container3.querySelectorAll(".bx--overflow-menu-options"),
|
|
];
|
|
|
|
expect(allMenus.length).toBeGreaterThan(0);
|
|
allMenus.forEach((menu) => {
|
|
expect(menu).toHaveStyle("--overflow-menu-options-after-width: 2rem");
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
["sm", "bx--overflow-menu--sm"],
|
|
["xl", "bx--overflow-menu--xl"],
|
|
] as const)("should support %s size", (size, expectedClass) => {
|
|
render(OverflowMenu, { props: { size } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
expect(menuButton).toHaveClass(expectedClass);
|
|
});
|
|
|
|
it("should not apply size classes when size is undefined", () => {
|
|
render(OverflowMenu, { props: { size: undefined } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
expect(menuButton).not.toHaveClass("bx--overflow-menu--sm");
|
|
expect(menuButton).not.toHaveClass("bx--overflow-menu--xl");
|
|
});
|
|
|
|
it("applies light variant styling", () => {
|
|
render(OverflowMenu, { props: { light: true } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
expect(menuButton).toHaveClass("bx--overflow-menu--light");
|
|
});
|
|
|
|
it("applies flipped styling", async () => {
|
|
render(OverflowMenu, { props: { flipped: true } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menu = screen.getByRole("menu");
|
|
expect(menu).toHaveClass("bx--overflow-menu--flip");
|
|
});
|
|
|
|
it("applies direction attribute", async () => {
|
|
render(OverflowMenu, { props: { direction: "top" } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menu = screen.getByRole("menu");
|
|
expect(menu).toHaveAttribute("data-floating-menu-direction", "top");
|
|
});
|
|
|
|
it("applies custom menu options class", async () => {
|
|
render(OverflowMenu, { props: { menuOptionsClass: "custom-class" } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menu = screen.getByRole("menu");
|
|
expect(menu).toHaveClass("custom-class");
|
|
});
|
|
|
|
it("applies custom icon class", () => {
|
|
render(OverflowMenu, { props: { iconClass: "custom-icon-class" } });
|
|
|
|
const icon = screen.getByRole("button").querySelector("svg");
|
|
expect(icon).toHaveClass("custom-icon-class");
|
|
});
|
|
|
|
it("uses custom icon description", () => {
|
|
render(OverflowMenu, { props: { iconDescription: "Custom description" } });
|
|
|
|
const icon = screen.getByRole("button").querySelector("svg");
|
|
expect(icon).toHaveAttribute("aria-label", "Custom description");
|
|
});
|
|
|
|
it("uses custom id", () => {
|
|
render(OverflowMenu, { props: { id: "custom-id" } });
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
expect(menuButton).toHaveAttribute("id", "custom-id");
|
|
});
|
|
|
|
it("applies danger styling to menu items", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menuItems = screen.getAllByRole("menuitem");
|
|
const dangerItem = menuItems.find(
|
|
(item) => item.textContent === "Delete service",
|
|
);
|
|
expect(dangerItem?.parentElement).toHaveClass(
|
|
"bx--overflow-menu-options__option--danger",
|
|
);
|
|
});
|
|
|
|
it("handles link menu items correctly", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menuItems = screen.getAllByRole("menuitem");
|
|
const linkItem = menuItems.find(
|
|
(item) => item.textContent === "API documentation",
|
|
);
|
|
expect(linkItem).toHaveAttribute(
|
|
"href",
|
|
"https://cloud.ibm.com/docs/api-gateway/",
|
|
);
|
|
});
|
|
|
|
it("returns focus to button after menu closes", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menuItems = screen.getAllByRole("menuitem");
|
|
expect(menuItems[0]).toHaveFocus();
|
|
|
|
await user.keyboard("{Escape}");
|
|
expect(menuButton).toHaveFocus();
|
|
});
|
|
|
|
it("handles close event with item click", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const spy = vi.spyOn(console, "log");
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
const menuItems = screen.getAllByRole("menuitem");
|
|
await user.click(menuItems[0]);
|
|
|
|
expect(spy).toHaveBeenCalledWith("close", {
|
|
index: 0,
|
|
text: "Manage credentials",
|
|
});
|
|
});
|
|
|
|
it("handles close event with escape key", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const spy = vi.spyOn(console, "log");
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
await user.keyboard("{Escape}");
|
|
|
|
expect(spy).toHaveBeenCalledWith("close", null);
|
|
});
|
|
|
|
it("handles close event with outside click", async () => {
|
|
render(OverflowMenu);
|
|
|
|
const spy = vi.spyOn(console, "log");
|
|
const menuButton = screen.getByRole("button");
|
|
await user.click(menuButton);
|
|
|
|
await user.click(document.body);
|
|
|
|
expect(spy).toHaveBeenCalledWith("close", null);
|
|
});
|
|
});
|