Files
fleet/frontend/components/Modal/Modal.tests.tsx
T
Juan Fernandez eb42b22230 Fix custom variable modal clearing when switching browser focus
Fixes #44805

Fixed a bug where the "Add custom variable" modal would clear entered
values when switching focus to another browser tab or application window
due to network refetches.
2026-06-05 16:11:30 -04:00

193 lines
6.1 KiB
TypeScript

import React from "react";
import { noop } from "lodash";
import { render, screen, fireEvent, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Modal from "./Modal";
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 },
]);
};
describe("Modal", () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it("renders title", () => {
render(
<Modal title="Foobar" onExit={noop}>
<div>test</div>
</Modal>
);
expect(screen.getByText("Foobar")).toBeVisible();
});
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 a backdrop mousedown is interrupted by the window losing focus", () => {
const onExit = jest.fn();
const { container } = render(
<Modal title="Test" onExit={onExit}>
<div>content</div>
</Modal>
);
const background = container.querySelector(".modal__background");
if (!background) throw new Error("Background element not found");
// User presses down on the backdrop then the window loses focus (tab switch /
// app switch) before releasing — mouseup never fires on the page.
fireEvent.mouseDown(background);
fireEvent.blur(window);
// On return, the stale mousedown state must not close the modal.
fireEvent.mouseUp(background);
act(() => jest.runAllTimers());
expect(onExit).not.toHaveBeenCalled();
});
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();
});
});