Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165import { describe, it, beforeEach, afterEach, mock } from "node:test";import assert from "node:assert/strict";import { HoverObserver } from "/js/hoverObserver.js";
// The observer bails when window.matchMedia("(hover: hover)").matches is false;// JSDOM's matchMedia returns { matches: false } by default. Force true here.let originalMatchMedia;function stubHoverEnabled(enabled) { window.matchMedia = () => ({ matches: enabled, media: "", addListener() {}, removeListener() {}, });}
function dispatchMove(target, { relatedTarget } = {}) { target.dispatchEvent( new window.MouseEvent("pointermove", { bubbles: true, relatedTarget }), );}function dispatchOut(target, { relatedTarget } = {}) { target.dispatchEvent( new window.MouseEvent("pointerout", { bubbles: true, relatedTarget }), );}
describe("HoverObserver", () => { let root; let a, b, other, inner; let onEnter, onLeave;
beforeEach(() => { originalMatchMedia = window.matchMedia; stubHoverEnabled(true); document.body.innerHTML = ""; root = document.createElement("div"); a = document.createElement("a"); a.className = "target"; a.setAttribute("data-hover", "a"); b = document.createElement("a"); b.className = "target"; b.setAttribute("data-hover", "b"); other = document.createElement("div"); // non-target inner = document.createElement("span"); // child of a a.appendChild(inner); root.append(a, b, other); document.body.appendChild(root); onEnter = mock.fn(); onLeave = mock.fn(); });
afterEach(() => { window.matchMedia = originalMatchMedia; });
function makeObserver(overrides = {}) { return new HoverObserver(root, { selector: ".target", onEnter, onLeave, ...overrides, }); }
it("fires onEnter when pointer moves onto a matching target", () => { makeObserver(); dispatchMove(a); assert.equal(onEnter.mock.callCount(), 1); assert.equal(onEnter.mock.calls[0].arguments[0], a); });
it("matches the closest ancestor so child hovers count", () => { makeObserver(); dispatchMove(inner); assert.equal(onEnter.mock.callCount(), 1); assert.equal(onEnter.mock.calls[0].arguments[0], a); });
it("does not fire again while pointer stays inside the same target", () => { makeObserver(); dispatchMove(a); dispatchMove(inner); dispatchMove(a); assert.equal(onEnter.mock.callCount(), 1); assert.equal(onLeave.mock.callCount(), 0); });
it("fires leave(A) then enter(B) when moving directly A → B", () => { makeObserver(); dispatchMove(a); dispatchMove(b); assert.deepEqual( onEnter.mock.calls.map((c) => c.arguments[0]), [a, b], ); assert.deepEqual( onLeave.mock.calls.map((c) => c.arguments[0]), [a], ); });
it("fires onLeave when the pointer moves onto a non-target sibling", () => { makeObserver(); dispatchMove(a); dispatchMove(other); assert.equal(onLeave.mock.callCount(), 1); assert.equal(onLeave.mock.calls[0].arguments[0], a); });
it("fires onLeave on pointerout when relatedTarget is outside the target", () => { makeObserver(); dispatchMove(a); dispatchOut(a, { relatedTarget: other }); assert.equal(onLeave.mock.callCount(), 1); });
it("ignores pointerout when relatedTarget is a child of the current target", () => { makeObserver(); dispatchMove(a); dispatchOut(a, { relatedTarget: inner }); assert.equal(onLeave.mock.callCount(), 0); });
it("does nothing when canHover() reports false", () => { stubHoverEnabled(false); makeObserver(); dispatchMove(a); assert.equal(onEnter.mock.callCount(), 0); });
it("respects the ignore predicate", () => { const ignore = mock.fn((target) => target === b); makeObserver({ ignore }); dispatchMove(a); dispatchMove(b); // Move to b was ignored → leave A fires, but no enter B. assert.deepEqual( onEnter.mock.calls.map((c) => c.arguments[0]), [a], ); assert.equal(onLeave.mock.callCount(), 1); });
it("disconnect() removes listeners and fires leave for the current target", () => { const obs = makeObserver(); dispatchMove(a); obs.disconnect(); assert.equal(onLeave.mock.callCount(), 1); // Further events do nothing. dispatchMove(b); assert.equal(onEnter.mock.callCount(), 1); });
it("clear() fires leave without removing listeners", () => { const obs = makeObserver(); dispatchMove(a); obs.clear(); assert.equal(onLeave.mock.callCount(), 1); // A subsequent enter on the same target still fires. dispatchMove(a); assert.equal(onEnter.mock.callCount(), 2); });});