Files
initiative/apps/web/src/__tests__/confirm-button.test.tsx
Lukas 36768d3aa1 Upgrade Biome to 2.4.7 and enable 54 additional lint rules
Add rules covering bug prevention (noLeakedRender, noFloatingPromises,
noImportCycles, noReactForwardRef), security (noScriptUrl, noAlert),
performance (noAwaitInLoops, useTopLevelRegex), and code style
(noNestedTernary, useGlobalThis, useNullishCoalescing, useSortedClasses,
plus ~40 more). Fix all violations: extract top-level regex constants,
guard React && renders with boolean coercion, refactor nested ternaries,
replace window with globalThis, sort Tailwind classes, and introduce
expectDomainError test helper to eliminate conditional expects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:25:09 +01:00

220 lines
5.3 KiB
TypeScript

// @vitest-environment jsdom
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ConfirmButton } from "../components/ui/confirm-button";
function XIcon() {
return <span data-testid="x-icon">X</span>;
}
function renderButton(
props: Partial<Parameters<typeof ConfirmButton>[0]> = {},
) {
const onConfirm = props.onConfirm ?? vi.fn();
render(
<ConfirmButton
icon={<XIcon />}
label="Remove combatant"
onConfirm={onConfirm}
{...props}
/>,
);
return { onConfirm };
}
describe("ConfirmButton", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
cleanup();
});
it("renders the default icon in idle state", () => {
renderButton();
expect(screen.getByTestId("x-icon")).toBeTruthy();
expect(screen.getByRole("button")).toHaveAttribute(
"aria-label",
"Remove combatant",
);
});
it("transitions to confirm state with Check icon on first click", () => {
renderButton();
fireEvent.click(screen.getByRole("button"));
expect(screen.queryByTestId("x-icon")).toBeNull();
expect(screen.getByRole("button")).toHaveAttribute(
"aria-label",
"Confirm remove combatant",
);
expect(screen.getByRole("button").className).toContain("bg-destructive");
});
it("calls onConfirm on second click in confirm state", () => {
const { onConfirm } = renderButton();
const button = screen.getByRole("button");
fireEvent.click(button);
fireEvent.click(button);
expect(onConfirm).toHaveBeenCalledOnce();
});
it("auto-reverts after 5 seconds", () => {
renderButton();
fireEvent.click(screen.getByRole("button"));
expect(screen.queryByTestId("x-icon")).toBeNull();
act(() => {
vi.advanceTimersByTime(5000);
});
expect(screen.getByTestId("x-icon")).toBeTruthy();
});
it("reverts on Escape key", () => {
renderButton();
fireEvent.click(screen.getByRole("button"));
expect(screen.queryByTestId("x-icon")).toBeNull();
fireEvent.keyDown(document, { key: "Escape" });
expect(screen.getByTestId("x-icon")).toBeTruthy();
});
it("reverts on click outside", () => {
renderButton();
fireEvent.click(screen.getByRole("button"));
expect(screen.queryByTestId("x-icon")).toBeNull();
fireEvent.mouseDown(document.body);
expect(screen.getByTestId("x-icon")).toBeTruthy();
});
it("does not enter confirm state when disabled", () => {
renderButton({ disabled: true });
fireEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("x-icon")).toBeTruthy();
});
it("cleans up timer on unmount", () => {
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
renderButton();
fireEvent.click(screen.getByRole("button"));
cleanup();
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
it("manages independent instances separately", () => {
const onConfirm1 = vi.fn();
const onConfirm2 = vi.fn();
render(
<>
<ConfirmButton
icon={<span data-testid="icon-1">1</span>}
label="Remove first"
onConfirm={onConfirm1}
/>
<ConfirmButton
icon={<span data-testid="icon-2">2</span>}
label="Remove second"
onConfirm={onConfirm2}
/>
</>,
);
const buttons = screen.getAllByRole("button");
fireEvent.click(buttons[0]);
// First is confirming, second is still idle
expect(screen.queryByTestId("icon-1")).toBeNull();
expect(screen.getByTestId("icon-2")).toBeTruthy();
});
// T008: Keyboard-specific tests
it("Enter key triggers confirm state", () => {
renderButton();
const button = screen.getByRole("button");
// Native button handles Enter via click event
fireEvent.click(button);
expect(screen.queryByTestId("x-icon")).toBeNull();
expect(button).toHaveAttribute("aria-label", "Confirm remove combatant");
});
it("Enter in confirm state calls onConfirm", () => {
const { onConfirm } = renderButton();
const button = screen.getByRole("button");
fireEvent.click(button); // enter confirm state
fireEvent.click(button); // confirm
expect(onConfirm).toHaveBeenCalledOnce();
});
it("Escape in confirm state reverts", () => {
renderButton();
fireEvent.click(screen.getByRole("button"));
fireEvent.keyDown(document, { key: "Escape" });
expect(screen.getByTestId("x-icon")).toBeTruthy();
expect(screen.getByRole("button")).toHaveAttribute(
"aria-label",
"Remove combatant",
);
});
it("blur event reverts confirm state", () => {
renderButton();
const button = screen.getByRole("button");
fireEvent.click(button);
expect(screen.queryByTestId("x-icon")).toBeNull();
fireEvent.blur(button);
expect(screen.getByTestId("x-icon")).toBeTruthy();
});
it("Enter/Space keydown stops propagation to prevent parent handlers", () => {
const parentHandler = vi.fn();
render(
// biome-ignore lint/a11y/noStaticElementInteractions: test wrapper
// biome-ignore lint/a11y/noNoninteractiveElementInteractions: test wrapper
<div onKeyDown={parentHandler}>
<ConfirmButton
icon={<XIcon />}
label="Remove combatant"
onConfirm={vi.fn()}
/>
</div>,
);
const button = screen.getByRole("button");
fireEvent.keyDown(button, { key: "Enter" });
fireEvent.keyDown(button, { key: " " });
expect(parentHandler).not.toHaveBeenCalled();
});
});