// @vitest-environment jsdom // // A hover surface (tooltip, hover card) is a PASSIVE layer. It still sits on the layer stack — // Escape closes it before the surface beneath — but it does not own the keyboard, so the page-level // keys (`j`/`k`, `s`, the `g` chords) keep working while one happens to be open under a resting pointer. // Without the flag, hovering any hinted icon silently disabled keyboard navigation. // // jsdom lays nothing out, and floating-ui's `hide` middleware then reports the reference as scrolled // out of view — and an invisible surface is deliberately NOT a layer (AnchoredPopover). A test on that // would pass without the flag, so the reference and the viewport get real boxes first, and the test // proves the surface is a layer (Escape closes it) before trusting the "not blocking" half. import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import Tooltip from "./Tooltip"; import { anyBlockingLayer } from "../../lib/layerStack"; const box = { x: 200, y: 200, top: 200, left: 200, width: 60, height: 24, right: 260, bottom: 224 }; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; vi.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue({ ...box, toJSON: () => box, } as DOMRect); for (const [prop, value] of [ ["clientWidth", 1280], ["clientHeight", 800], ] as const) { Object.defineProperty(document.documentElement, prop, { configurable: true, value }); } }); afterAll(() => vi.restoreAllMocks()); afterEach(cleanup); describe("an open tooltip", () => { it("is a layer Escape reaches, but does not block the page keys", async () => { render( , ); const trigger = screen.getByText("icon").parentElement as HTMLElement; act(() => { fireEvent.pointerEnter(trigger); fireEvent.mouseEnter(trigger); fireEvent.mouseMove(trigger); }); const surface = await screen.findByRole("tooltip"); await waitFor(() => expect(surface.style.visibility).not.toBe("hidden")); expect(anyBlockingLayer()).toBe(false); act(() => { fireEvent.keyDown(document, { key: "Escape" }); }); await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull()); }); }); // On an iPad a tap's synthetic mouseenter showed the caption; WebKit then took the tap as a hover and // swallowed its click (every rail button needed two taps), and with no mouseleave the caption stayed. describe("a tooltip under a finger", () => { class PointerEventStub extends MouseEvent { pointerType: string; constructor(type: string, init: PointerEventInit = {}) { super(type, init); this.pointerType = init.pointerType ?? ""; } } const hover = (trigger: HTMLElement, pointerType: string) => act(() => { trigger.dispatchEvent(new PointerEventStub("pointerdown", { bubbles: true, pointerType })); trigger.dispatchEvent(new PointerEventStub("pointerenter", { pointerType })); fireEvent.mouseEnter(trigger); fireEvent.mouseMove(trigger); }); const mount = () => { render( , ); return screen.getByText("icon").parentElement as HTMLElement; }; it("does not open from a tap", async () => { hover(mount(), "touch"); await new Promise((r) => setTimeout(r, 50)); expect(screen.queryByRole("tooltip")).toBeNull(); }); it("still opens under a mouse", async () => { hover(mount(), "mouse"); expect(await screen.findByRole("tooltip")).toBeTruthy(); }); });