diff --git a/src/components/popover-container/popover-container.spec.tsx b/src/components/popover-container/popover-container.spec.tsx deleted file mode 100644 index 6b9328c16a..0000000000 --- a/src/components/popover-container/popover-container.spec.tsx +++ /dev/null @@ -1,859 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { forwardRef } from "react"; -import { mount, ReactWrapper } from "enzyme"; -import { act } from "react-dom/test-utils"; -import { CSSTransition } from "react-transition-group"; - -import Popover from "../../__internal__/popover"; -import { - PopoverContainerContentStyle, - PopoverContainerCloseIcon, - PopoverContainerOpenIcon, - PopoverContainerWrapperStyle, - PopoverContainerTitleStyle, -} from "./popover-container.style"; -import StyledIcon from "../icon/icon.style"; -import PopoverContainer, { - PopoverContainerProps, - RenderOpenProps, - RenderCloseProps, -} from "./popover-container.component"; -import { - assertStyleMatch, - testStyledSystemPadding, -} from "../../__spec_helper__/__internal__/test-utils"; -import Icon from "../icon"; -import guid from "../../__internal__/utils/helpers/guid"; -import { Select, Option } from "../select"; -import useMediaQuery from "../../hooks/useMediaQuery"; -import rootTagTest from "../../__internal__/utils/helpers/tags/tags-specs"; - -jest.mock("../../hooks/useMediaQuery", () => { - return { - __esModule: true, - default: jest.fn().mockReturnValue(false), - }; -}); - -jest.mock("../../__internal__/utils/helpers/guid"); -(guid as jest.MockedFunction).mockImplementation(() => "guid-123"); - -const render = (props?: PopoverContainerProps) => { - return mount( - - ); -}; - -const renderAttached = (props?: PopoverContainerProps) => { - return mount( - , - { attachTo: document.getElementById("enzymeContainer") } - ); -}; - -describe("PopoverContainer", () => { - testStyledSystemPadding( - (props) => ( - -
children
-
- ), - { p: "16px 24px" }, - (wrapper) => wrapper.find(PopoverContainerContentStyle) - ); - - let wrapper: ReactWrapper; - let onOpenFn: jest.Mock | undefined, onCloseFn: jest.Mock | undefined; - - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(() => { - wrapper = render(); - }); - - afterEach(() => { - jest.runOnlyPendingTimers(); - onOpenFn?.mockClear(); - onCloseFn?.mockClear(); - wrapper.unmount(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it("should render correct", () => { - expect(wrapper.exists()).toBe(true); - }); - - it("should render correct title", () => { - wrapper = render({ open: true }); - expect(wrapper.find(PopoverContainerTitleStyle).text()).toBe( - "PopoverContainerSettings" - ); - }); - - it("should render correct `aria-describedby", () => { - wrapper = render({ open: true, ariaDescribedBy: "myAriaDescribedBy" }); - expect( - wrapper.find(PopoverContainerContentStyle).prop("aria-describedby") - ).toBe("myAriaDescribedBy"); - }); - - it("should render correct children", () => { - wrapper = mount( - -
children
-
- ); - - expect(wrapper.find("#myChildren").exists()).toBe(true); - }); - - it("should unmount after 300ms when the component closes", () => { - expect(wrapper.find(CSSTransition).props().timeout).toEqual({ exit: 300 }); - expect(wrapper.find(CSSTransition).props().unmountOnExit).toBe(true); - }); - - it("should transition when the component opens and closes", () => { - expect(wrapper.find(CSSTransition).props().in).toBe(false); - - act(() => { - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - }); - - wrapper.update(); - - expect(wrapper.find(CSSTransition).props().in).toBe(true); - }); - - it("should render correct `id` based on `guid()`", () => { - wrapper = render({ open: true }); - expect(wrapper.find(PopoverContainerTitleStyle).props().id).toBe( - "PopoverContainer_guid-123" - ); - }); - - it("should render correct `data-element` related to the component", () => { - wrapper = render({ open: true }); - expect(wrapper.find(PopoverContainerTitleStyle).prop("data-element")).toBe( - "popover-container-title" - ); - }); - - it("should render correct `data-component` related to the component", () => { - expect( - wrapper.find(PopoverContainerWrapperStyle).prop("data-component") - ).toBe("popover-container"); - }); - - it("should set the id for `PopoverContainerTitleStyle` when title has a value", () => { - wrapper = render({ open: true }); - expect(wrapper.find(PopoverContainerTitleStyle).props().id).toBe( - "PopoverContainer_guid-123" - ); - }); - - it("should not set the id for `PopoverContainerTitleStyle` when title has no value", () => { - wrapper = render({ - open: true, - title: undefined, - openButtonAriaLabel: "foo", - containerAriaLabel: "bar", - }); - expect(wrapper.find(PopoverContainerTitleStyle).props().id).toBe(undefined); - }); - - it("should let opening button to be focusable if popover is closed", () => { - wrapper = render({ open: false }); - - expect(wrapper.find("button").props().tabIndex).toBe(0); - }); - - it("`shouldCoverButton` should be false by default", () => { - act(() => { - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - }); - - wrapper.update(); - - expect(wrapper.find(PopoverContainerContentStyle).props().modifiers).toBe( - undefined - ); - }); - - it("should allow custom data props on close button to be assigned", () => { - wrapper = render({ - open: true, - closeButtonDataProps: { - "data-element": "foo", - "data-role": "bar", - }, - }); - const closeButton = wrapper.find("[data-component='close']").first(); - - rootTagTest(closeButton, "close", "foo", "bar"); - }); - - describe("popover", () => { - it("renders a DayPicker inside of a Popover", () => { - wrapper = render({ open: true }); - - expect( - wrapper.find(Popover).find(PopoverContainerContentStyle).exists() - ).toBe(true); - }); - - it("should have the correct offset when shouldCoverButton is false", () => { - wrapper = render({ shouldCoverButton: false, open: true }); - - expect(wrapper.find(Popover).props().middleware?.[0]?.options).toBe(6); - }); - - it("should have the correct offset when shouldCoverButton is set to true", () => { - wrapper = render({ shouldCoverButton: true, open: true }); - - expect(wrapper.find(Popover).props().middleware?.[0]?.options).not.toBe( - undefined - ); - - const rects = { reference: { height: 40, width: 0, y: 0, x: 0 } }; - - expect( - wrapper.find(Popover).props().middleware?.[0]?.options?.({ - rects, - }) - ).toEqual({ mainAxis: -40 }); - }); - - it("should have the expected strategy when disabledAnimation is true", () => { - wrapper = render({ disableAnimation: true, open: true }); - - expect(wrapper.find(Popover).props().popoverStrategy)?.toBe("fixed"); - }); - - it("should have the expected strategy when disabledAnimation is false and user doesn't allow motion or their preference cannot be determined", () => { - const mockUseMediaQuery = useMediaQuery as jest.MockedFunction< - typeof useMediaQuery - >; - mockUseMediaQuery.mockReturnValueOnce(false); - - wrapper = render({ disableAnimation: false, open: true }); - - expect(wrapper.find(Popover).props().popoverStrategy)?.toBe("fixed"); - }); - - it("should have the expected strategy when disabledAnimation is false and user allows motion", () => { - const mockUseMediaQuery = useMediaQuery as jest.MockedFunction< - typeof useMediaQuery - >; - mockUseMediaQuery.mockReturnValueOnce(true); - - wrapper = render({ disableAnimation: false, open: true }); - - expect(wrapper.find(Popover).props().popoverStrategy)?.toBe("absolute"); - }); - - it.each([ - ["bottom-start", "right"] as const, - ["bottom-end", "left"] as const, - ])( - "should have placement equal to %s when position prop is equal %s", - (placement, position) => { - wrapper = render({ position, open: true }); - - expect(wrapper.find(Popover).props().placement).toEqual(placement); - } - ); - }); - - describe("if is controlled", () => { - describe("and is opened", () => { - describe("and `onClose` prop do not exists", () => { - it("should not error when open button is clicked and no `onClose` callback is provided", () => { - expect(() => { - wrapper = render({ - open: true, - }); - - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - }).not.toThrow(); - }); - }); - - describe("and `onClose` prop is provided", () => { - it("should fire `onClose` callback if open button is clicked", () => { - onCloseFn = jest.fn(); - wrapper = render({ - open: true, - onClose: onCloseFn, - }); - - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - expect(onCloseFn).toHaveBeenCalled(); - }); - - it("should fire `onClose` callback if close button is clicked", () => { - onCloseFn = jest.fn(); - wrapper = render({ - open: true, - onClose: onCloseFn, - }); - - wrapper.find(PopoverContainerCloseIcon).props().onClick(); - expect(onCloseFn).toHaveBeenCalled(); - }); - }); - }); - - describe("and is closed", () => { - describe("and `onOpen` prop is provided", () => { - it("should fire `onOpen` callback if open button is clicked", () => { - onOpenFn = jest.fn(); - wrapper = render({ - open: false, - onOpen: onOpenFn, - }); - - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - expect(onOpenFn).toHaveBeenCalled(); - }); - }); - - describe("and `onOpen` prop is not provided", () => { - it("should not error when open button is clicked if no `onOpen` callback is provided", () => { - expect(() => { - wrapper = render({ - open: false, - }); - - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - }).not.toThrow(); - }); - }); - }); - }); - - describe("if is not controlled", () => { - it("should render default open button with the expected prop values", () => { - wrapper = render(); - const openIcon = wrapper.find(PopoverContainerOpenIcon); - - expect(openIcon.exists()).toBe(true); - expect(openIcon.find(Icon).props()).toEqual({ type: "settings" }); - expect(openIcon.prop("aria-haspopup")).toEqual("dialog"); - expect(openIcon.prop("tabIndex")).toEqual(0); - expect(openIcon.prop("aria-label")).toEqual("PopoverContainerSettings"); - }); - - it("should render default close button", () => { - wrapper = render(); - - act(() => { - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - }); - - wrapper.update(); - expect(wrapper.find(PopoverContainerCloseIcon).exists()).toBe(true); - }); - - it("should open popover if open button is clicked", () => { - wrapper = render(); - - act(() => { - wrapper.find(PopoverContainerOpenIcon).props().onClick(); - }); - - wrapper.update(); - expect(wrapper.find(PopoverContainerContentStyle).exists()).toBe(true); - }); - - describe("and custom component is provided as an opening button", () => { - interface MyOpenButtonProps extends RenderOpenProps { - children: React.ReactNode; - } - - const MyOpenButton = React.forwardRef< - HTMLButtonElement, - MyOpenButtonProps - >((props: MyOpenButtonProps, ref) => ( - + )} + > + Ta da! +
+ ); + + expect(screen.getByRole("button")).toHaveTextContent("Custom Open Button"); + }); + + it("consumer can associate custom open button with popup", () => { + render( + ( + + )} + > + Ta da! + + ); + + const openButton = screen.getByRole("button"); + expect(openButton).toHaveAttribute("aria-haspopup", "dialog"); + }); + + it("consumer can set aria-expanded attribute on custom open button", () => { + render( + ( + + )} + > + Ta da! + + ); + + const openButton = screen.getByRole("button"); + expect(openButton).toHaveAttribute("aria-expanded", "false"); + }); +}); + +test("popup has a title when title prop is passed", () => { + render( + + Ta da! + + ); + + expect(screen.getByRole("dialog")).toHaveTextContent("Custom Title"); +}); + +test("popup uses title prop as its correct accessible name when passed", () => { + render( + + Ta da! + + ); + + expect(screen.getByRole("dialog")).toHaveAccessibleName("Custom Title"); +}); + +test("popup uses containerAriaLabel prop as its correct accessible name when passed", () => { + render( + + Ta da! + + ); + + expect(screen.getByRole("dialog")).toHaveAccessibleName("Custom Label"); +}); + +test("popup has correct accessible description when ariaDescribedBy prop is passed", () => { + render( + <> +

Custom Subtitle

+ + Ta da! + + + ); + + expect(screen.getByRole("dialog")).toHaveAccessibleDescription( + "Custom Subtitle" + ); +}); + +test("popup title has correct data tag", () => { + render( + + Ta da! + + ); + + expect(screen.getByText("Custom Title")).toHaveAttribute( + "data-element", + "popover-container-title" + ); +}); + +describe("close button", () => { + it("renders close button in popup when onClose prop is passed", () => { + render( + {}} open> + Ta da! + + ); + + expect(screen.getByRole("button", { name: "close" })).toBeVisible(); + }); + + it("renders custom close button provided by renderCloseComponent prop", () => { + render( + ( + + )} + > + Ta da! + + ); + + expect( + screen.getByRole("button", { name: "Custom Close Button" }) + ).toBeVisible(); + }); + + it("has correct data tags, when the prop closeButtonDataProps is provided", () => { + render( + + Content + + ); + + expect(screen.getByRole("button", { name: "close" })).toHaveAttribute( + "data-element", + "foo" + ); + expect(screen.getByRole("button", { name: "close" })).toHaveAttribute( + "data-role", + "bar" + ); + }); +}); + test("does not animate in popup when disableAnimation is true", async () => { const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime, @@ -29,57 +230,313 @@ test("does not animate in popup when disableAnimation is true", async () => { }); }); -test("closes popup when Escape key is pressed", async () => { - const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); +test("does not animate in popup if user consent for animations cannot be determined", async () => { + const mockedUseMediaQuery = jest.mocked(useMediaQuery); + mockedUseMediaQuery.mockReturnValue(false); + + const user = userEvent.setup({ + advanceTimers: jest.advanceTimersByTime, + }); render(Content); await user.click(screen.getByRole("button")); - await screen.findByRole("dialog"); - - await user.keyboard("{Escape}"); - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + expect(await screen.findByRole("dialog")).toHaveStyle({ + position: "fixed", + opacity: "1", + transform: "none", }); + + mockedUseMediaQuery.mockReset(); }); -test("pressing Escape key does not close the popup, when nested popup content is open inside it", async () => { +test("popup traps focus when shouldCoverButton prop is true", async () => { const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); render( - {}}> - + {}}> + Content ); - await user.click(screen.getByRole("button")); + const closeButton = screen.getByRole("button", { name: "close" }); + fireEvent.focus(closeButton); - // open select list - await user.click(await screen.findByRole("combobox")); - await screen.findByRole("listbox"); + await user.tab(); - // should close select list only - await user.keyboard("{Escape}"); + expect(closeButton).toHaveFocus(); +}); - expect(screen.getByRole("dialog")).toBeVisible(); +test("popup allows outside focus when shouldCoverButton prop is false", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + {}}> + Content + + ); + + const closeButton = screen.getByRole("button", { name: "close" }); + fireEvent.focus(closeButton); + + await user.tab(); + + expect(closeButton).not.toHaveFocus(); }); -test("triggers closing animation sequence with correct timing when closing popup", async () => { +test.each([ + ["left", "bottom-end"], + ["right", "bottom-start"], +] as const)( + "computes popup positioning correctly when position prop is '%s'", + async (position, placement) => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); + + await user.click(screen.getByRole("button")); + + expect(await screen.findByRole("dialog")).toHaveAttribute( + "data-floating-placement", + placement + ); + } +); + +test("popup visibility is controllable via open prop", async () => { + const ControlledPopoverContainer = () => { + const [open, setOpen] = React.useState(false); + + return ( + <> + + {}}> + Content + + å + + ); + }; const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); - render(Content); + render(); - await user.click(screen.getByRole("button")); + const toggleButton = screen.getByRole("button", { name: "Toggle popup" }); + await user.click(toggleButton); + + expect(await screen.findByRole("dialog")).toBeVisible(); + + await user.click(toggleButton); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); +}); + +describe("opening the popup", () => { + it("opens popup when open button is clicked", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Ta da!); + + await user.click(screen.getByRole("button")); + + const popup = await screen.findByRole("dialog"); + expect(popup).toBeVisible(); + expect(popup).toHaveTextContent("Ta da!"); + }); + + it("opens popup when open button is selected via the keyboard", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Ta da!); + + await user.tab(); + await user.keyboard("{Enter}"); + + const popup = await screen.findByRole("dialog"); + expect(popup).toBeVisible(); + expect(popup).toHaveTextContent("Ta da!"); + }); + + it("calls onOpen callback when popup is opened", async () => { + const onOpen = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); + + await user.click(screen.getByRole("button")); + + await waitFor(() => { + expect(onOpen).toHaveBeenCalledTimes(1); + }); + }); + + it("open button still has focus after popup is opened", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Ta da!); + + const button = screen.getByRole("button"); + await user.click(button); + await screen.findByRole("dialog"); + + expect(button).toHaveFocus(); + }); +}); + +describe("closing the popup", () => { + it("closes popup when close button is clicked", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); + + await user.click(screen.getByRole("button")); + + await user.click(await screen.findByRole("button", { name: "close" })); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("closes popup when close button is selected via the keyboard", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); - const popup = await screen.findByRole("dialog"); - await user.click(screen.getByRole("button", { name: "close" })); + await user.click(screen.getByRole("button")); - expect(popup).toHaveClass("exit"); + const closeButton = await screen.findByRole("button", { name: "close" }); + closeButton.focus(); - act(() => { - jest.advanceTimersByTime(300); + await user.keyboard("{Enter}"); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("closes popup when Escape key is pressed", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); + + await user.click(screen.getByRole("button")); + await screen.findByRole("dialog"); + + await user.keyboard("{Escape}"); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("closes popup when outside content is clicked", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + <> + {}}>Content +

Outside popup

+ + ); + + await user.click(screen.getByRole("button")); + await screen.findByRole("dialog"); + + await user.click(screen.getByText("Outside popup")); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("focuses open button after popup is closed", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Ta da!); + + const button = screen.getByRole("button"); + await user.click(button); + + await user.click(await screen.findByRole("button", { name: "close" })); + + expect(button).toHaveFocus(); + }); + + it("calls onClose callback when popup is closed", async () => { + const onClose = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); + + await user.click(screen.getByRole("button")); + + await user.click(await screen.findByRole("button", { name: "close" })); + + await waitFor(() => { + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + + it("closes popup when open button is clicked twice", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + {}}> + Content + + ); + + const openButton = screen.getByRole("button", { name: "open popup" }); + await user.click(openButton); + await user.click(openButton); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + it("pressing Escape key does not close the popup, when nested popup content is open inside it", async () => { + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + {}}> + + + ); + + await user.click(screen.getByRole("button")); + + // open select list + await user.click(await screen.findByRole("combobox")); + await screen.findByRole("listbox"); + + // should close select list only + await user.keyboard("{Escape}"); + + expect(screen.getByRole("dialog")).toBeVisible(); }); - expect(popup).toHaveClass("exit-done"); + it("triggers closing animation sequence with correct timing when closing popup", async () => { + const mockedUseMediaQuery = jest.mocked(useMediaQuery); + mockedUseMediaQuery.mockReturnValue(true); + + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(Content); + + await user.click(screen.getByRole("button")); + + const popup = await screen.findByRole("dialog"); + await user.click(screen.getByRole("button", { name: "close" })); + + expect(popup).toHaveClass("exit"); + + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(popup).toHaveClass("exit-done"); + mockedUseMediaQuery.mockReset(); + }); }); + +testStyledSystemPadding( + (props) => ( + + Ta da! + + ), + { p: "16px 24px" }, + (wrapper) => wrapper.find("[role='dialog']") +);