/*
* Copyright 2015 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { mount, type ReactWrapper, shallow, type ShallowWrapper } from "enzyme";
import { assert, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
import { Button } from "../button/buttons";
import { Icon } from "../icon/icon";
import { PopoverInteractionKind } from "../popover/popoverProps";
import { PopoverNext } from "../popover-next/popoverNext";
import { Text } from "../text/text";
import { type MenuProps } from "./menu";
import { MenuItem, type MenuItemProps } from "./menuItem";
describe("MenuItem", () => {
it("basic rendering", () => {
const wrapper = shallow();
assert.isTrue(wrapper.find(Icon).exists());
assert.strictEqual(findText(wrapper).text(), "Graph");
});
it("supports HTML props", () => {
const mouseHandler = (_event: React.MouseEvent) => false;
const keyHandler = (_event: React.KeyboardEvent) => false;
const item = shallow(
,
).find("a");
assert.strictEqual(item.prop("onClick"), mouseHandler);
assert.strictEqual(item.prop("onKeyDown"), keyHandler);
assert.strictEqual(item.prop("onMouseMove"), mouseHandler);
});
it("children appear in submenu", () => {
const wrapper = shallow(
,
);
const submenu = findSubmenu(wrapper);
assert.lengthOf(submenu.props.children, 3);
});
it("default role prop structure is correct for a menuitem that is a an item of a ul with role=menu", () => {
const wrapper = mount();
assert.equal(wrapper.find("li").prop("role"), "none");
assert.equal(wrapper.find("a").prop("role"), "menuitem");
});
it("can set roleStructure to change role prop structure to that of a listbox or select item", () => {
const wrapper = mount();
assert.equal(wrapper.find("li").prop("role"), "option");
assert.isUndefined(wrapper.find("a").prop("role"));
});
it("can set roleStructure to change role prop structure to that of a list item", () => {
const wrapper = mount();
assert.isUndefined(wrapper.find("li").prop("role"));
assert.isUndefined(wrapper.find("a").prop("role"));
});
it('can set roleStructure to change role prop structure to void li role (set role="none")', () => {
const wrapper = mount();
assert.equal(wrapper.find("li").prop("role"), "none");
assert.isUndefined(wrapper.find("a").prop("role"));
});
it("disabled MenuItem will not show its submenu", () => {
const wrapper = shallow(
,
);
assert.isTrue(wrapper.find(PopoverNext).prop("disabled"));
});
it("disabled MenuItem blocks mouse listeners", () => {
const mouseSpy = vi.fn();
mount()
.simulate("click")
.simulate("mouseenter")
.simulate("click");
expect(mouseSpy).not.toHaveBeenCalled();
});
it("clicking MenuItem triggers onClick prop", () => {
const onClick = vi.fn();
shallow()
.find("a")
.simulate("click");
expect(onClick).toHaveBeenCalledOnce();
});
it("pressing enter on MenuItem triggers onClick prop", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render();
const menuItem = screen.getByRole("menuitem");
menuItem.focus();
await user.keyboard("{Enter}");
expect(onClick).toHaveBeenCalledOnce();
});
it("clicking disabled MenuItem does not trigger onClick prop", () => {
const onClick = vi.fn();
shallow()
.find("a")
.simulate("click");
expect(onClick).not.toHaveBeenCalled();
});
it("shouldDismissPopover=false prevents a clicked MenuItem from closing the Popover automatically", () => {
const handleClose = vi.fn();
const menu = ;
const wrapper = mount(
,
);
wrapper.find(MenuItem).find("a").simulate("click");
expect(handleClose).not.toHaveBeenCalled();
});
it("submenuProps are forwarded to the Menu", () => {
const submenuProps = { "aria-label": "test-menu" };
const wrapper = shallow(
,
);
const submenu = findSubmenu(wrapper);
assert.strictEqual(submenu.props["aria-label"], submenuProps["aria-label"]);
});
it("popoverProps (except content) are forwarded to Popover", () => {
// Ensures that popover props are passed to Popover component, except content property
const popoverProps = {
content: "CUSTOM_CONTENT",
interactionKind: PopoverInteractionKind.CLICK,
popoverClassName: "CUSTOM_POPOVER_CLASS_NAME",
};
const wrapper = shallow(
,
);
assert.strictEqual(wrapper.find(PopoverNext).prop("interactionKind"), popoverProps.interactionKind);
assert.notStrictEqual(
wrapper.find(PopoverNext).prop("popoverClassName")!.indexOf(popoverProps.popoverClassName),
0,
);
assert.notStrictEqual(wrapper.find(PopoverNext).prop("content"), popoverProps.content);
});
it("multiline prop determines if long content is ellipsized", () => {
const wrapper = mount(
,
);
function assertOverflow(expected: boolean) {
assert.strictEqual(findText(wrapper).hasClass(Classes.TEXT_OVERFLOW_ELLIPSIS), expected);
}
assertOverflow(true);
wrapper.setProps({ multiline: true });
assertOverflow(false);
});
it(`label and labelElement are rendered in .${Classes.MENU_ITEM_LABEL}`, () => {
const wrapper = shallow(
label element} />,
);
const label = wrapper.find(`.${Classes.MENU_ITEM_LABEL}`);
assert.match(label.text(), /^label text/);
assert.strictEqual(label.find("article").text(), "label element");
});
it("renders icon with aria-hidden attribute on wrapper span", () => {
const wrapper = mount();
const iconWrapper = wrapper.find(`.${Classes.MENU_ITEM_ICON}`);
assert.strictEqual(iconWrapper.prop("aria-hidden"), true);
});
it("renders custom icon element with aria-hidden attribute on wrapper span", () => {
const customIcon = Custom;
const wrapper = mount();
const iconWrapper = wrapper.find(`.${Classes.MENU_ITEM_ICON}`);
assert.strictEqual(iconWrapper.prop("aria-hidden"), true);
});
describe("tabIndex behavior", () => {
it("MenuItem without submenu has tabIndex={0} when enabled", () => {
const { container } = render();
const anchor = container.querySelector("a");
assert.strictEqual(anchor?.getAttribute("tabindex"), "0");
});
it("MenuItem without submenu has tabIndex={-1} when disabled", () => {
const { container } = render();
const anchor = container.querySelector("a");
assert.strictEqual(anchor?.getAttribute("tabindex"), "-1");
});
it("MenuItem with submenu has focusable Popover target when enabled", () => {
const { container } = render(
,
);
// The Popover target wrapper should be focusable
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
assert.strictEqual(popoverTarget?.getAttribute("tabindex"), "0");
});
it("MenuItem with submenu has tabIndex={-1} on inner anchor element", () => {
const { getByText } = render(
,
);
// The inner anchor should NOT be focusable when there's a submenu
const textElement = getByText("Parent");
const anchor = textElement.closest("a");
assert.strictEqual(anchor?.getAttribute("tabindex"), "-1");
});
it("MenuItem with disabled submenu is not focusable", () => {
const { container, getByText } = render(
,
);
const parentElement = getByText("Parent");
const parentAnchor = parentElement.closest("a");
assert.strictEqual(parentAnchor?.getAttribute("tabindex"), "-1");
// When disabled, the Popover target should not be in the tab order
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
// The target exists but disabled state is handled by the Popover component
assert.isNotNull(popoverTarget);
});
it("MenuItem without submenu preserves custom tabIndex", () => {
const { container } = render();
const anchor = container.querySelector("a");
assert.strictEqual(anchor?.getAttribute("tabindex"), "3");
});
});
});
function findSubmenu(wrapper: ShallowWrapper) {
return wrapper.find(PopoverNext).prop("content") as React.ReactElement<
MenuProps & { children: Array> }
>;
}
function findText(wrapper: ShallowWrapper | ReactWrapper) {
return wrapper.find(Text).children();
}