test: add coverage for gitlooper ticket sweep

- useFocusTrap.test.ts: 7 unit tests covering rAF-deferred focus,
  Tab/Shift+Tab wrapping, empty-list guard, and cleanup (#56)
- nextConfig.test.ts: 3 tests for /login and /blueprints redirects (#51 #52);
  5 tests for COMPLETED demand exclusion filter logic (#66)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-03 17:33:27 +02:00
parent 9241f22993
commit bc0bb5bdb8
2 changed files with 322 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Capture effect callbacks so we can invoke them manually — same pattern as
// useViewportPopover.test.ts / useTimelineSSE.test.ts.
const effectCleanups: Array<() => void> = [];
vi.mock("react", async () => {
const actual = await vi.importActual<typeof import("react")>("react");
return {
...actual,
useEffect: (callback: () => void | (() => void)) => {
const cleanup = callback();
if (typeof cleanup === "function") {
effectCleanups.push(cleanup);
}
},
};
});
import { useFocusTrap } from "./useFocusTrap.js";
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Build a minimal focusable HTMLElement stub. */
function makeFocusable(focused = false): HTMLElement {
return {
focus: vi.fn(),
tagName: "BUTTON",
disabled: false,
tabIndex: 0,
matches: () => true,
} as unknown as HTMLElement;
}
interface MockContainer {
querySelectorAll: ReturnType<typeof vi.fn>;
addEventListener: ReturnType<typeof vi.fn>;
removeEventListener: ReturnType<typeof vi.fn>;
_emit(type: string, event: Partial<KeyboardEvent>): void;
}
/**
* Build a minimal container element whose querySelectorAll returns the given
* list of focusable children, and whose addEventListener / removeEventListener
* can be spied on.
*/
function makeContainer(focusable: HTMLElement[]): MockContainer {
const listeners = new Map<string, EventListener>();
return {
querySelectorAll: vi.fn((_selector: string) => focusable),
addEventListener: vi.fn((type: string, handler: EventListener) => {
listeners.set(type, handler);
}),
removeEventListener: vi.fn((type: string) => {
listeners.delete(type);
}),
_emit(type: string, event: Partial<KeyboardEvent>) {
listeners.get(type)?.(event as Event);
},
};
}
// ─── Tests ───────────────────────────────────────────────────────────────────
describe("useFocusTrap", () => {
let rafId = 0;
let rafCallbacks: Array<() => void>;
beforeEach(() => {
vi.clearAllMocks();
effectCleanups.length = 0;
rafCallbacks = [];
rafId = 0;
// Stub rAF: capture callbacks without executing them immediately so we
// control when initial focus fires.
vi.stubGlobal("requestAnimationFrame", (cb: () => void) => {
rafCallbacks.push(cb);
return ++rafId;
});
vi.stubGlobal("cancelAnimationFrame", vi.fn());
});
afterEach(() => {
// Run all pending cleanups to avoid leaking listeners between tests.
while (effectCleanups.length > 0) {
effectCleanups.pop()?.();
}
vi.unstubAllGlobals();
});
// ── isOpen = false → no rAF, no listener ──────────────────────────────────
it("does nothing when isOpen is false", () => {
const first = makeFocusable();
const container = makeContainer([first]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, false);
expect(rafCallbacks).toHaveLength(0);
expect(container.addEventListener).not.toHaveBeenCalled();
});
// ── isOpen = true → deferred focus on first element ──────────────────────
it("focuses the first focusable element via rAF when isOpen is true", () => {
const first = makeFocusable();
const second = makeFocusable();
const container = makeContainer([first, second]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, true);
// rAF should have been queued but not yet fired
expect(rafCallbacks).toHaveLength(1);
expect(first.focus).not.toHaveBeenCalled();
// Fire the rAF callback to simulate browser layout completion
const rafCb = rafCallbacks[0];
if (!rafCb) throw new Error("expected rAF callback to be queued");
rafCb();
expect(first.focus).toHaveBeenCalledTimes(1);
expect(second.focus).not.toHaveBeenCalled();
});
// ── Tab key wraps forward (last → first) ──────────────────────────────────
it("wraps focus from last to first on Tab", () => {
const first = makeFocusable();
const last = makeFocusable();
const container = makeContainer([first, last]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, true);
// Simulate Tab with document.activeElement on the last element
vi.stubGlobal("document", { activeElement: last });
const preventDefault = vi.fn();
container._emit("keydown", { key: "Tab", shiftKey: false, preventDefault });
expect(preventDefault).toHaveBeenCalled();
expect(first.focus).toHaveBeenCalledTimes(1);
});
// ── Shift+Tab key wraps backward (first → last) ──────────────────────────
it("wraps focus from first to last on Shift+Tab", () => {
const first = makeFocusable();
const last = makeFocusable();
const container = makeContainer([first, last]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, true);
// Simulate Shift+Tab with document.activeElement on the first element
vi.stubGlobal("document", { activeElement: first });
const preventDefault = vi.fn();
container._emit("keydown", { key: "Tab", shiftKey: true, preventDefault });
expect(preventDefault).toHaveBeenCalled();
expect(last.focus).toHaveBeenCalledTimes(1);
});
// ── Non-Tab key → ignored ─────────────────────────────────────────────────
it("ignores non-Tab keys", () => {
const first = makeFocusable();
const container = makeContainer([first]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, true);
vi.stubGlobal("document", { activeElement: first });
const preventDefault = vi.fn();
container._emit("keydown", { key: "Escape", shiftKey: false, preventDefault });
expect(preventDefault).not.toHaveBeenCalled();
});
// ── Empty focusable list → preventDefault, no focus attempt ──────────────
it("calls preventDefault and does not throw when there are no focusable elements", () => {
const container = makeContainer([]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, true);
vi.stubGlobal("document", { activeElement: null });
const preventDefault = vi.fn();
expect(() => {
container._emit("keydown", { key: "Tab", shiftKey: false, preventDefault });
}).not.toThrow();
expect(preventDefault).toHaveBeenCalled();
});
// ── Cleanup cancels rAF and removes listener ──────────────────────────────
it("cancels pending rAF and removes keydown listener on cleanup", () => {
const first = makeFocusable();
const container = makeContainer([first]);
const ref = { current: container as unknown as HTMLElement };
useFocusTrap(ref, true);
expect(container.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
// Run cleanup (simulates component unmount or isOpen → false)
effectCleanups.pop()?.();
expect(cancelAnimationFrame).toHaveBeenCalledWith(rafId);
expect(container.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
});
});