Skip to content

Commit

Permalink
test(menu): Added tests for the new menu API and fixed a few issues
Browse files Browse the repository at this point in the history
test(form): Added test for new MenuItem components
  • Loading branch information
mlaursen committed Jan 30, 2022
1 parent c27bf55 commit 7202dd0
Show file tree
Hide file tree
Showing 31 changed files with 5,138 additions and 93 deletions.
14 changes: 11 additions & 3 deletions packages/form/src/menu/MenuItemFileInput.tsx
Expand Up @@ -13,12 +13,17 @@ export type MenuItemAllowedFileInputProps = Pick<
"id" | "onChange" | "accept" | "multiple" | "disableRepeatableFiles"
>;

/**
* @remarks \@since 5.0.0
*/
/** @remarks \@since 5.0.0 */
export interface MenuItemFileInputProps
extends Omit<MenuItemProps, "id" | "onChange">,
MenuItemAllowedFileInputProps {
/**
* An `aria-label` to apply to the `<input type="file">`.
*
* @defaultValue `"Upload"`
*/
inputLabel?: string;

/**
* Any additional props that should be passed to the `<input type="file">`
* element. You probably won't ever need to use this.
Expand Down Expand Up @@ -50,6 +55,7 @@ export const MenuItemFileInput = forwardRef<
inputProps,
disableRepeatableFiles = false,
leftAddon: propLeftAddon,
inputLabel = "Upload",
...props
},
ref
Expand All @@ -68,12 +74,14 @@ export const MenuItemFileInput = forwardRef<
// `<input type="file">` to select a file and the menu unmounts when
// hidden.
event.stopPropagation();
/* istanbul-ignore-next */
inputRef.current?.click();
}}
leftAddon={leftAddon}
>
{children}
<input
aria-label={inputLabel}
id={id}
ref={inputRef}
type="file"
Expand Down
8 changes: 4 additions & 4 deletions packages/form/src/menu/MenuItemTextField.tsx
Expand Up @@ -20,6 +20,10 @@ export interface MenuItemTextFieldProps extends TextFieldProps {
* This is a wrapper for the `TextField` component that can be used within
* `Menu`s by updating the `onKeyDown` and `onClick` behavior.
*
* Note: This is **not** the `TextFieldWithMessage` since the message part is
* hard to style nicely within menus. You'd most likely want to use another menu
* for displaying errors.
*
* @remarks \@since 5.0.0
*/
export const MenuItemTextField = forwardRef<
Expand All @@ -46,10 +50,6 @@ export const MenuItemTextField = forwardRef<
stretch={stretch}
onKeyDown={(event) => {
onKeyDown?.(event);
if (event.isPropagationStopped()) {
return;
}

switch (event.key) {
case "Tab":
case "Escape":
Expand Down
76 changes: 76 additions & 0 deletions packages/form/src/menu/__tests__/MenuItemFileInput.tsx
@@ -0,0 +1,76 @@
import { Configuration } from "@react-md/layout";
import { DropdownMenu } from "@react-md/menu";
import {
fireEvent,
render as baseRender,
RenderResult,
} from "@testing-library/react";
import { FC, ReactElement } from "react";
import {
MenuItemFileInput,
MenuItemFileInputProps,
} from "../MenuItemFileInput";

const Wrapper: FC = ({ children }) => (
<Configuration disableRipple>{children}</Configuration>
);

function render(ui: ReactElement): RenderResult {
return baseRender(ui, { wrapper: Wrapper });
}

type TestProps = Omit<MenuItemFileInputProps, "id">;

function Test(props: TestProps): ReactElement {
return (
<DropdownMenu id="dropdown-menu" buttonChildren="Dropdown" timeout={0}>
<MenuItemFileInput id="file-input" {...props} />
</DropdownMenu>
);
}

describe("MenuItemFileInput", () => {
it("should work correctly", () => {
const onChange = jest.fn();
const { getByRole, getByLabelText } = render(<Test onChange={onChange} />);

const dropdown = getByRole("button", { name: "Dropdown" });
fireEvent.click(dropdown);
const menu = getByRole("menu", { name: "Dropdown" });
expect(menu).toMatchSnapshot();

fireEvent.change(getByLabelText("Upload"));
expect(onChange).toBeCalledTimes(1);
expect(menu).toBeInTheDocument();
});

it("should call the onClick handlers correctly", () => {
const onClick = jest.fn();
const onInputClick = jest.fn();
const onChange = jest.fn();
const { getByRole, getByLabelText } = render(
<Test
onClick={onClick}
onChange={onChange}
inputProps={{ onClick: onInputClick }}
/>
);

const dropdown = getByRole("button", { name: "Dropdown" });
fireEvent.click(dropdown);
const menu = getByRole("menu", { name: "Dropdown" });
expect(menu).toMatchSnapshot();

fireEvent.click(getByRole("menuitem"));
expect(onClick).toBeCalledTimes(1);
expect(onInputClick).toBeCalledTimes(1);
expect(onChange).not.toBeCalled();
expect(menu).toBeInTheDocument();

fireEvent.click(getByLabelText("Upload"));
expect(onClick).toBeCalledTimes(1);
expect(onInputClick).toBeCalledTimes(2);
expect(onChange).not.toBeCalled();
expect(menu).toBeInTheDocument();
});
});
116 changes: 116 additions & 0 deletions packages/form/src/menu/__tests__/MenuItemTextField.tsx
@@ -0,0 +1,116 @@
import { Configuration } from "@react-md/layout";
import { DropdownMenu, MenuItem } from "@react-md/menu";
import {
fireEvent,
render as baseRender,
RenderResult,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { FC, ReactElement } from "react";
import {
MenuItemTextField,
MenuItemTextFieldProps,
} from "../MenuItemTextField";

const Wrapper: FC = ({ children }) => (
<Configuration disableRipple>{children}</Configuration>
);

function render(ui: ReactElement): RenderResult {
return baseRender(ui, { wrapper: Wrapper });
}

function Test({
label = "Label",
placeholder = "Placeholder",
liProps: propLiProps,
...props
}: Partial<MenuItemTextFieldProps>): ReactElement {
const liProps = {
"data-testid": "li",
...propLiProps,
} as const;
return (
<DropdownMenu id="dropdown-menu" buttonChildren="Dropdown" timeout={0}>
<MenuItem>Before</MenuItem>
<MenuItemTextField
id="text-field"
{...props}
liProps={liProps}
label={label}
placeholder={placeholder}
/>
<MenuItem>After</MenuItem>
</DropdownMenu>
);
}

describe("MenuItemTextField", () => {
it("should not change the menu's keyboard focus behavior while there is no value", () => {
const { getByRole } = render(<Test />);

const dropdown = getByRole("button", { name: "Dropdown" });
fireEvent.click(dropdown);
const menu = getByRole("menu", { name: "Dropdown" });
expect(menu).toMatchSnapshot();

const itemBefore = getByRole("menuitem", { name: "Before" });
const itemAfter = getByRole("menuitem", { name: "After" });
const textField = getByRole("textbox", { name: "Label" });

fireEvent.keyDown(menu, { key: "ArrowDown" });
expect(document.activeElement).toBe(itemBefore);

fireEvent.keyDown(itemBefore, { key: "A" });
expect(document.activeElement).toBe(itemAfter);

fireEvent.keyDown(itemAfter, { key: "ArrowUp" });
expect(document.activeElement).toBe(textField);

fireEvent.keyDown(textField, { key: "ArrowUp" });
expect(document.activeElement).toBe(itemBefore);

fireEvent.keyDown(itemAfter, { key: "ArrowDown" });
expect(document.activeElement).toBe(textField);

fireEvent.keyDown(textField, { key: "End" });
expect(document.activeElement).toBe(itemAfter);

fireEvent.keyDown(itemAfter, { key: "ArrowUp" });
expect(document.activeElement).toBe(textField);

fireEvent.change(textField, { target: { value: "letters" } });
fireEvent.keyDown(textField, { key: "Home" });
expect(document.activeElement).toBe(textField);

fireEvent.keyDown(textField, { key: "End" });
expect(document.activeElement).toBe(textField);

fireEvent.keyDown(textField, { key: "ArrowLeft" });
expect(document.activeElement).toBe(textField);

fireEvent.keyDown(textField, { key: "ArrowRight" });
expect(document.activeElement).toBe(textField);

userEvent.type(textField, "a");
expect(document.activeElement).toBe(textField);
expect(textField).toHaveValue("lettersa");

fireEvent.keyDown(textField, { key: "Escape" });
expect(menu).not.toBeInTheDocument();
});

it("should not cause the menu to close when the text fiedl or li is clicked", () => {
const { getByRole, getByTestId } = render(<Test />);
fireEvent.click(getByRole("button", { name: "Dropdown" }));

const menu = getByRole("menu", { name: "Dropdown" });
expect(menu).toBeInTheDocument();

fireEvent.click(getByRole("textbox", { name: "Label" }));
expect(menu).toBeInTheDocument();

fireEvent.click(getByTestId("li"));
expect(menu).toBeInTheDocument();
});
});
@@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MenuItemFileInput should call the onClick handlers correctly 1`] = `
<div
aria-labelledby="dropdown-menu"
class="rmd-menu rmd-menu--elevated"
id="dropdown-menu-menu"
role="menu"
style="left: 16px; top: 0px; position: fixed; transform-origin: 0 0;"
tabindex="-1"
>
<ul
class="rmd-list"
role="none"
>
<li
class="rmd-list-item rmd-list-item--medium rmd-list-item--clickable rmd-menu-item"
id="file-input-menuitem"
role="menuitem"
tabindex="-1"
>
<i
aria-hidden="true"
class="rmd-icon rmd-icon--font material-icons rmd-icon--before rmd-list-item__addon rmd-list-item__addon--before"
>
file_upload
</i>
<span
class="rmd-list-item__text"
>
<input
aria-label="Upload"
class="rmd-file-input"
id="file-input"
type="file"
value=""
/>
</span>
<span
class="rmd-ripple-container"
/>
</li>
</ul>
</div>
`;

exports[`MenuItemFileInput should work correctly 1`] = `
<div
aria-labelledby="dropdown-menu"
class="rmd-menu rmd-menu--elevated"
id="dropdown-menu-menu"
role="menu"
style="left: 16px; top: 0px; position: fixed; transform-origin: 0 0;"
tabindex="-1"
>
<ul
class="rmd-list"
role="none"
>
<li
class="rmd-list-item rmd-list-item--medium rmd-list-item--clickable rmd-menu-item"
id="file-input-menuitem"
role="menuitem"
tabindex="-1"
>
<i
aria-hidden="true"
class="rmd-icon rmd-icon--font material-icons rmd-icon--before rmd-list-item__addon rmd-list-item__addon--before"
>
file_upload
</i>
<span
class="rmd-list-item__text"
>
<input
aria-label="Upload"
class="rmd-file-input"
id="file-input"
type="file"
value=""
/>
</span>
<span
class="rmd-ripple-container"
/>
</li>
</ul>
</div>
`;

0 comments on commit 7202dd0

Please sign in to comment.