2026-01-08 21:37:46 +00:00
|
|
|
import React from "react";
|
|
|
|
|
import { noop } from "lodash";
|
2026-05-05 13:27:24 +00:00
|
|
|
import { render, screen, fireEvent, act } from "@testing-library/react";
|
|
|
|
|
import userEvent from "@testing-library/user-event";
|
2026-01-08 21:37:46 +00:00
|
|
|
|
|
|
|
|
import Modal from "./Modal";
|
|
|
|
|
|
2026-05-05 13:27:24 +00:00
|
|
|
const clickBackground = async (background: Element | null) => {
|
|
|
|
|
if (!background) throw new Error("Background element not found");
|
|
|
|
|
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
|
|
|
|
await user.pointer([
|
|
|
|
|
{ keys: "[MouseLeft>]", target: background },
|
|
|
|
|
{ keys: "[/MouseLeft]", target: background },
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-08 21:37:46 +00:00
|
|
|
describe("Modal", () => {
|
2026-05-05 13:27:24 +00:00
|
|
|
beforeEach(() => jest.useFakeTimers());
|
|
|
|
|
afterEach(() => jest.useRealTimers());
|
|
|
|
|
|
2026-01-08 21:37:46 +00:00
|
|
|
it("renders title", () => {
|
|
|
|
|
render(
|
|
|
|
|
<Modal title="Foobar" onExit={noop}>
|
|
|
|
|
<div>test</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText("Foobar")).toBeVisible();
|
|
|
|
|
});
|
2026-05-05 13:27:24 +00:00
|
|
|
|
|
|
|
|
it("calls onExit when clicking the background overlay", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<div>content</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not call onExit when clicking inside the modal container", () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<div>content</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Simulate drag that starts inside the container and releases on the background.
|
|
|
|
|
// The container stops mouseDown propagation so isDownOnBackgroundRef stays false,
|
|
|
|
|
// meaning the background's mouseUp handler must not fire onExit.
|
|
|
|
|
fireEvent.mouseDown(screen.getByText("content"));
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
if (!background) throw new Error("Background element not found");
|
|
|
|
|
fireEvent.mouseUp(background);
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not call onExit when clicking the background if a form inside has been interacted with", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<form>
|
|
|
|
|
<input type="text" />
|
|
|
|
|
</form>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const input = screen.getByRole("textbox");
|
|
|
|
|
fireEvent.input(input, { target: { value: "hello" } });
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not call onExit when clicking the background if a text input outside a form has been interacted with", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<div>
|
|
|
|
|
<input type="text" />
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const input = screen.getByRole("textbox");
|
|
|
|
|
fireEvent.input(input, { target: { value: "hello" } });
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not call onExit when clicking the background if a checkbox has been checked", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<div>
|
|
|
|
|
<input type="checkbox" />
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const checkbox = screen.getByRole("checkbox");
|
|
|
|
|
fireEvent.click(checkbox);
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not call onExit when clicking the background if a toggle has been clicked", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<div>
|
|
|
|
|
<button type="button" role="switch" aria-checked={false}>
|
|
|
|
|
Toggle
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const toggle = screen.getByRole("switch");
|
|
|
|
|
fireEvent.click(toggle);
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("calls onExit when clicking the background if a form inside has not been interacted with", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit}>
|
|
|
|
|
<form>
|
|
|
|
|
<input type="text" />
|
|
|
|
|
</form>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not call onExit when clicking the background if disableClosingModal is true", async () => {
|
|
|
|
|
const onExit = jest.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<Modal title="Test" onExit={onExit} disableClosingModal>
|
|
|
|
|
<div>content</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const background = container.querySelector(".modal__background");
|
|
|
|
|
await clickBackground(background);
|
|
|
|
|
act(() => jest.runAllTimers());
|
|
|
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
|
|
|
});
|
2026-01-08 21:37:46 +00:00
|
|
|
});
|