Skip to content

Commit

Permalink
context menu tests passing
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte committed May 30, 2024
1 parent 83277ca commit 8aad2aa
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 111 deletions.
2 changes: 2 additions & 0 deletions packages/bits-ui/src/lib/bits/context-menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as RadioGroup } from "$lib/bits/menu/components/menu-radio-grou
export { default as SubContent } from "$lib/bits/menu/components/menu-sub-content.svelte";
export { default as SubTrigger } from "$lib/bits/menu/components/menu-sub-trigger.svelte";
export { default as CheckboxItem } from "$lib/bits/menu/components/menu-checkbox-item.svelte";
export { default as Portal } from "$lib/bits/utilities/portal/portal.svelte";

export type {
ContextMenuArrowProps as ArrowProps,
Expand All @@ -28,4 +29,5 @@ export type {
ContextMenuSubTriggerProps as SubTriggerProps,
ContextMenuContentProps as ContentProps,
ContextMenuTriggerProps as TriggerProps,
ContextMenuPortalProps as PortalProps,
} from "./types.js";
2 changes: 2 additions & 0 deletions packages/bits-ui/src/lib/bits/context-menu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type {
SubContentProps as ContextMenuSubContentProps,
SubProps as ContextMenuSubProps,
SubTriggerProps as ContextMenuSubTriggerProps,
PortalProps as ContextMenuPortalProps,
} from "$lib/bits/menu/index.js";

export type {
Expand All @@ -42,4 +43,5 @@ export type {
MenuSubPropsWithoutHTML as ContextMenuSubPropsWithoutHTML,
MenuSubTriggerPropsWithoutHTML as ContextMenuSubTriggerPropsWithoutHTML,
MenuSubContentPropsWithoutHTML as ContextMenuSubContentPropsWithoutHTML,
MenuPortalPropsWithoutHTML as ContextMenuPortalPropsWithoutHTML,
} from "$lib/bits/menu/types.js";
9 changes: 0 additions & 9 deletions packages/bits-ui/src/lib/bits/menu/menu.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,20 +910,11 @@ class ContextMenuTriggerState {
this.#clearLongPressTimer();
};

#ariaControls = $derived.by(() => {
if (this.#parentMenu.open.value && this.#parentMenu.contentNode.value)
return this.#parentMenu.contentNode.value.id;
return undefined;
});

props = $derived.by(
() =>
({
id: this.#parentMenu.triggerId.value,
disabled: this.#disabled.value,
"aria-haspopup": "menu",
"aria-expanded": getAriaExpanded(this.#parentMenu.open.value),
"aria-controls": this.#ariaControls,
"data-disabled": getDataDisabled(this.#disabled.value),
"data-state": getDataOpenClosed(this.#parentMenu.open.value),
[TRIGGER_ATTR]: "",
Expand Down
84 changes: 51 additions & 33 deletions packages/bits-ui/src/tests/context-menu/ContextMenu.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/svelte";
import { render, screen, waitFor } from "@testing-library/svelte/svelte5";
import { userEvent } from "@testing-library/user-event";
import { axe } from "jest-axe";
import { describe, it } from "vitest";
Expand All @@ -12,7 +12,7 @@ const kbd = getTestKbd();
* Helper function to reduce boilerplate in tests
*/
function setup(props: ContextMenuTestProps = {}) {
const user = userEvent.setup();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const { getByTestId, queryByTestId } = render(ContextMenuTest, { ...props });
const trigger = getByTestId("trigger");
return {
Expand Down Expand Up @@ -73,7 +73,6 @@ describe("context menu", () => {
"checkbox-item",
"radio-group",
"radio-item",
"checkbox-indicator",
];

for (const part of parts) {
Expand Down Expand Up @@ -123,20 +122,20 @@ describe("context menu", () => {
const { getByTestId, user, trigger } = await open();
const checkedBinding = getByTestId("checked-binding");
const indicator = getByTestId("checkbox-indicator");
expect(indicator).not.toHaveTextContent("checked");
expect(indicator).not.toHaveTextContent("true");
expect(checkedBinding).toHaveTextContent("false");
const checkbox = getByTestId("checkbox-item");
await user.click(checkbox);
expect(checkedBinding).toHaveTextContent("true");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
expect(indicator).toHaveTextContent("checked");
expect(indicator).toHaveTextContent("true");
await user.click(getByTestId("checkbox-item"));
expect(checkedBinding).toHaveTextContent("false");

await user.click(checkedBinding);
expect(checkedBinding).toHaveTextContent("true");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
expect(getByTestId("checkbox-indicator")).toHaveTextContent("checked");
expect(getByTestId("checkbox-indicator")).toHaveTextContent("true");
});

it("toggles checkbox items within submenus when clicked & respects binding", async () => {
Expand All @@ -146,50 +145,44 @@ describe("context menu", () => {
const subCheckedBinding = getByTestId("sub-checked-binding");
expect(subCheckedBinding).toHaveTextContent("false");
const indicator = getByTestId("sub-checkbox-indicator");
expect(indicator).not.toHaveTextContent("checked");
expect(indicator).not.toHaveTextContent("true");
const subCheckbox = getByTestId("sub-checkbox-item");
await user.click(subCheckbox);
expect(subCheckedBinding).toHaveTextContent("true");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
await openSubmenu(props);
expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("checked");
expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("true");
await user.click(getByTestId("sub-checkbox-item"));
expect(subCheckedBinding).toHaveTextContent("false");

await user.click(subCheckedBinding);
expect(subCheckedBinding).toHaveTextContent("true");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
await openSubmenu(props);
expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("checked");
expect(getByTestId("sub-checkbox-indicator")).toHaveTextContent("true");
});

it("checks the radio item when clicked & respects binding", async () => {
const { getByTestId, queryByTestId, user, trigger } = await open();
const radioBinding = getByTestId("radio-binding");
const indicator = queryByTestId("radio-indicator-1");
expect(indicator).toBeNull();
expect(radioBinding).toHaveTextContent("");
const radioItem1 = getByTestId("radio-item");
await user.click(radioItem1);
expect(radioBinding).toHaveTextContent("1");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
const radioIndicator = getByTestId("radio-indicator-1");
expect(radioIndicator).not.toBeNull();
expect(radioIndicator).toHaveTextContent("checked");
expect(radioIndicator).toHaveTextContent("true");
const radioItem2 = getByTestId("radio-item-2");
await user.click(radioItem2);
expect(radioBinding).toHaveTextContent("2");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
expect(queryByTestId("radio-indicator-1")).toBeNull();
expect(queryByTestId("radio-indicator-2")).toHaveTextContent("checked");
expect(queryByTestId("radio-indicator-2")).toHaveTextContent("true");

await user.keyboard(kbd.ESCAPE);
expect(queryByTestId("content")).toBeNull();
await user.click(radioBinding);
expect(radioBinding).toHaveTextContent("");
await user.pointer([{ target: trigger }, { keys: "[MouseRight]", target: trigger }]);
expect(queryByTestId("radio-indicator-1")).toBeNull();
expect(queryByTestId("radio-indicator-2")).toBeNull();
});

it("skips over disabled items when navigating with the keyboard", async () => {
Expand All @@ -203,8 +196,12 @@ describe("context menu", () => {
expect(getByTestId("disabled-item-2")).not.toHaveFocus();
});

it("doesnt loop through the menu items when the `loop` prop is set to false/undefined", async () => {
const { user, getByTestId } = await open();
it("doesnt loop through the menu items when the `loop` prop is set to false", async () => {
const { user, getByTestId } = await open({
contentProps: {
loop: false,
},
});
await user.keyboard(kbd.ARROW_DOWN);
await user.keyboard(kbd.ARROW_DOWN);
await waitFor(() => expect(getByTestId("sub-trigger")).toHaveFocus());
Expand All @@ -221,7 +218,11 @@ describe("context menu", () => {
});

it("loops through the menu items when the `loop` prop is set to true", async () => {
const { user, getByTestId } = await open({ loop: true });
const { user, getByTestId } = await open({
contentProps: {
loop: true,
},
});
await user.keyboard(kbd.ARROW_DOWN);
await waitFor(() => expect(getByTestId("item")).toHaveFocus());
await user.keyboard(kbd.ARROW_DOWN);
Expand All @@ -244,39 +245,56 @@ describe("context menu", () => {
expect(queryByTestId("content")).toBeNull();
});

it("respects the `closeOnEscape` prop", async () => {
const { queryByTestId, user } = await open({ closeOnEscape: false });
it("respects the `escapeKeydownBehavior: 'ignore'` prop", async () => {
const { queryByTestId, user } = await open({
contentProps: {
escapeKeydownBehavior: "ignore",
},
});
await user.keyboard(kbd.ESCAPE);
expect(queryByTestId("content")).not.toBeNull();
});

it("respects the `closeOnOutsideClick` prop", async () => {
it("respects the `interactOutsideBehavior: 'ignore'` prop", async () => {
const { queryByTestId, user, getByTestId } = await open({
closeOnOutsideClick: false,
contentProps: {
interactOutsideBehavior: "ignore",
},
});
const outside = getByTestId("outside");
await user.click(outside);
expect(queryByTestId("content")).not.toBeNull();
});

it("portals to the body if a `portal` prop is not passed", async () => {
it("portals to the body if a `to` prop is not passed to the Portal", async () => {
const { getByTestId } = await open();
const content = getByTestId("content");
expect(content.parentElement).toEqual(document.body);
const wrapper = content.parentElement;
expect(wrapper?.parentElement).toEqual(document.body);
});

it("portals to the portal target if a valid `portal` prop is passed", async () => {
const { getByTestId } = await open({ portal: "#portal-target" });
it("portals to the portal target if a valid `to` prop is passed to the portal", async () => {
const { getByTestId } = await open({
portalProps: {
to: "#portal-target",
},
});
const content = getByTestId("content");
const wrapper = content.parentElement;
const portalTarget = getByTestId("portal-target");
expect(content.parentElement).toEqual(portalTarget);
expect(wrapper?.parentElement).toEqual(portalTarget);
});

it("does not portal if `null` is passed as the portal prop", async () => {
const { getByTestId } = await open({ portal: null });
it("does not portal if `disabled: true` is passed to the Portal", async () => {
const { getByTestId } = await open({
portalProps: {
disabled: true,
},
});
const content = getByTestId("content");
const ogContainer = getByTestId("non-portal-container");
expect(content.parentElement).not.toEqual(document.body);
expect(content.parentElement).toEqual(ogContainer);
const wrapper = content.parentElement;
expect(wrapper?.parentElement).not.toEqual(document.body);
expect(wrapper?.parentElement).toEqual(ogContainer);
});
});
Loading

0 comments on commit 8aad2aa

Please sign in to comment.