= [];
const { getByTestId } = render(
seen.push(e.clientY)} />,
);
fireEvent.pointerDown(getByTestId("probe"), { clientY: 100, pointerId: 7 });
// Asserted as "not a finite number" rather than against a literal, because
// the literal is a trap: an earlier probe here printed `null` only because
// JSON.stringify rewrites an undefined array entry that way. The value is
// actually `undefined`, and the guard has to survive both.
expect(seen).toHaveLength(1);
expect(Number.isFinite(seen[0])).toBe(false);
});
it("a press with no usable coordinate starts no drag at all", () => {
// The consequence of the line above. Without the guard, `startY` holds
// null, every later handler's "has a drag begun" check answers yes, and
// the arithmetic runs on null.
const onClose = vi.fn();
const { container, getByRole } = render(
x
,
);
const panel = getByRole("dialog").querySelector(".strand-sheet__panel") as HTMLElement;
const grab = (container.querySelector(".strand-sheet__grab") as HTMLElement);
fireEvent.pointerDown(grab, { clientY: 100, pointerId: 1 });
fireEvent.pointerMove(grab, { clientY: 400, pointerId: 1 });
fireEvent.pointerUp(grab, { clientY: 400, pointerId: 1 });
expect(onClose).not.toHaveBeenCalled();
expect(panel.getAttribute("style") || "").not.toContain("translateY");
});
// ── Inherited behaviour, which is the reason this composes Dialog ──
it("Escape closes the sheet, inherited rather than rebuilt", () => {
const onClose = vi.fn();
const { getByRole } = render(
x
,
);
fireEvent.keyDown(getByRole("dialog"), { key: "Escape" });
expect(onClose).toHaveBeenCalled();
});
it("the page behind cannot scroll while the sheet is open", () => {
render(x);
expect(document.body.style.overflow).toBe("hidden");
});
it("no close X is rendered, because the pattern dismisses by gesture, Escape or backdrop", () => {
const { getByRole } = render(x);
expect(getByRole("dialog").querySelector(".strand-dialog__close")).toBeNull();
});
});
// ── The drag decision, exhausted ──
//
// Every rule the gesture has lives here, at no cost and with no DOM. The
// handlers above are the plumbing that feeds this; this is the behaviour.
describe("dragOutcome", () => {
const HEIGHT = 500; // threshold at 0.28 is 140
it("a drag past the threshold dismisses the sheet", () => {
expect(dragOutcome(100, 400, HEIGHT).dismiss).toBe(true);
});
it("a short drag springs back, so a graze on the grabber is not a close", () => {
expect(dragOutcome(100, 130, HEIGHT).dismiss).toBe(false);
});
it("exactly at the threshold does NOT dismiss, because a boundary has to fall somewhere", () => {
expect(dragOutcome(0, HEIGHT * DISMISS_FRACTION, HEIGHT).dismiss).toBe(false);
});
it("one pixel past the threshold does dismiss", () => {
expect(dragOutcome(0, HEIGHT * DISMISS_FRACTION + 1, HEIGHT).dismiss).toBe(true);
});
it("dragging UPWARD never dismisses, because the sheet is already at its height", () => {
expect(dragOutcome(400, 100, HEIGHT).dismiss).toBe(false);
});
it("an upward drag reports zero travel rather than a negative offset", () => {
// The travel value is written to `transform: translateY`. A negative one
// would lift the panel off the edge it is anchored to.
expect(dragOutcome(400, 100, HEIGHT).travelled).toBe(0);
});
it("the same gesture means the same thing on a short phone and a tall one", () => {
// 200px is over the threshold on a 500px sheet and under it on a 1000px
// one. A pixel constant would make the gesture mean different things on
// different devices, which is the reason the rule is a fraction.
expect(dragOutcome(0, 200, 500).dismiss).toBe(true);
expect(dragOutcome(0, 200, 1000).dismiss).toBe(false);
});
it("a press that never began cannot dismiss anything", () => {
expect(dragOutcome(null, 400, HEIGHT).dismiss).toBe(false);
});
it("an unmeasurable coordinate is NOT a drag of zero, it is no drag", () => {
// The distinction is load-bearing: treating a missing coordinate as zero
// makes the arithmetic run on null and the gesture die silently in any
// environment that reports no coordinates.
expect(dragOutcome(Number.NaN, 400, HEIGHT)).toEqual({ travelled: 0, dismiss: false });
expect(dragOutcome(100, Number.NaN, HEIGHT)).toEqual({ travelled: 0, dismiss: false });
});
it("a sheet that measures zero cannot be dismissed by an idle press", () => {
// A zero height would make the threshold zero, so ANY travel at all would
// close the sheet, including the one-pixel jitter a resting thumb makes.
expect(dragOutcome(100, 100, 0).dismiss).toBe(false);
});
it("the threshold is overridable, because a taller sheet may want a longer pull", () => {
expect(dragOutcome(0, 300, HEIGHT, 0.5).dismiss).toBe(true);
expect(dragOutcome(0, 200, HEIGHT, 0.5).dismiss).toBe(false);
});
});
import { snapshotFixtures } from "../../test/snapshot.js";
import { snapshotStylesheet } from "../../test/stylesheet.js";
import { fixtures } from "./Sheet.fixtures.js";
snapshotFixtures(Sheet, fixtures);
snapshotStylesheet(resolve(__dirname, "./Sheet.css"));