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>
220 lines
5.3 KiB
TypeScript
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();
|
|
});
|
|
});
|