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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226import { describe, it, beforeEach, afterEach } from "node:test";import assert from "node:assert/strict";
// scrollLocks.js coordinates holders through a module-level manager.const { scrollLocks } = await import("/js/scrollLocks.js?fresh-for-test");
describe("ScrollLock", () => { let container; let createdLocks;
const createLock = (target) => { const lock = scrollLocks.acquire({ target }); createdLocks.push(lock); return lock; };
beforeEach(() => { container = document.createElement("div"); container.className = "current-page"; container.innerHTML = "<header></header><main></main>"; document.body.appendChild(container); scrollLocks.setContainerProvider(() => container); createdLocks = []; document.body.style.position = ""; });
afterEach(() => { for (const lock of createdLocks) { lock.release(); } container.remove(); document.body.style.position = ""; document.body.style.overflow = ""; document.body.style.top = ""; document.body.style.width = ""; document.body.style.height = ""; });
it("locks and unlocks page scroll", () => { const lock = createLock(); assert.deepEqual(document.body.style.position, "fixed"); lock.release(); assert.deepEqual(document.body.style.position, ""); });
it("keeps the page locked when the first holder unlocks before the second", () => { const menuLock = createLock(); const dialogLock = createLock();
menuLock.release(); assert.deepEqual(document.body.style.position, "fixed");
dialogLock.release(); assert.deepEqual(document.body.style.position, ""); });
it("keeps the page locked when a stacked dialog unlocks first", () => { const composerLock = createLock(); const nestedDialogLock = createLock();
nestedDialogLock.release(); assert.deepEqual(document.body.style.position, "fixed");
composerLock.release(); assert.deepEqual(document.body.style.position, ""); });
it("returns a harmless release when no page container is available", () => { scrollLocks.setContainerProvider(() => null);
const warn = console.warn; console.warn = () => {}; try { const lock = createLock(); assert.deepEqual(document.body.style.position, "");
lock.release(); assert.deepEqual(document.body.style.position, ""); } finally { console.warn = warn; } });
it("ignores repeated releases from the same lease", () => { const lock = createLock(); lock.release(); lock.release(); assert.deepEqual(document.body.style.position, ""); });
it("compensates with the scroll position from before the header is pinned", () => { // Pinning the sticky header removes it from flow, and at the bottom of // the page the browser clamps scrollY down by the header height — // simulate that clamp to ensure the margin uses the pre-pin value. const header = container.querySelector("header"); const descriptor = Object.getOwnPropertyDescriptor(window, "scrollY"); Object.defineProperty(window, "scrollY", { configurable: true, get: () => header.classList.contains("scroll-lock-pinned") ? 3553.5 : 3600, }); try { createLock(); const main = container.querySelector("main"); assert.deepEqual(main.style.marginTop, "-3600px"); } finally { if (descriptor) { Object.defineProperty(window, "scrollY", descriptor); } else { delete window.scrollY; } } });
describe("with a fixed header", () => { let scrollYDescriptor; let originalScrollTo; let scrollToCalls;
beforeEach(() => { const header = container.querySelector("header"); header.style.position = "fixed"; header.getBoundingClientRect = () => ({ height: 100 }); scrollYDescriptor = Object.getOwnPropertyDescriptor(window, "scrollY"); Object.defineProperty(window, "scrollY", { configurable: true, get: () => 3600, }); originalScrollTo = window.scrollTo; scrollToCalls = []; window.scrollTo = (...args) => scrollToCalls.push(args); });
afterEach(() => { if (scrollYDescriptor) { Object.defineProperty(window, "scrollY", scrollYDescriptor); } else { delete window.scrollY; } window.scrollTo = originalScrollTo; });
it("doesn't compensate for the header height when pinning", () => { createLock(); const main = container.querySelector("main"); assert.deepEqual(main.style.marginTop, "-3600px"); });
it("leaves the header's inline position alone", () => { const header = container.querySelector("header"); header.style.left = "5px"; header.style.width = "300px"; const lock = createLock(); assert(!header.classList.contains("scroll-lock-pinned")); lock.release(); assert.deepEqual(header.style.left, "5px"); assert.deepEqual(header.style.width, "300px"); });
it("restores the scroll position on unlock", () => { const main = container.querySelector("main"); main.getBoundingClientRect = () => ({ top: -3500 }); const lock = createLock(); lock.release(); assert.deepEqual(scrollToCalls[0], [0, 3600]); });
// Locked, main sits where its margin puts it; unlocked, its rect follows // its document position and the scroll position const mockMainRect = (main, layout) => { main.getBoundingClientRect = () => ({ top: main.style.marginTop ? layout.lockedTop : layout.documentTop - window.scrollY, }); };
it("follows main if it moved while locked", () => { const main = container.querySelector("main"); const layout = { lockedTop: -3500, documentTop: 100 }; mockMainRect(main, layout); const lock = createLock(); layout.lockedTop = -3540; lock.release(); assert.deepEqual(scrollToCalls[0], [0, 3640]); });
it("ignores a stale lock-time rect (iOS WebKit)", () => { const main = container.querySelector("main"); // Seen on iOS: right after locking, the rect is still offset by scrollY main.getBoundingClientRect = () => ({ top: 100 }); const lock = createLock(); mockMainRect(main, { lockedTop: -3500, documentTop: 100 }); lock.release(); assert.deepEqual(scrollToCalls[0], [0, 3600]); });
it("keeps main in place when content above it changed while locked", () => { const main = container.querySelector("main"); const layout = { lockedTop: -3500, documentTop: 100 }; mockMainRect(main, layout); const lock = createLock(); // 300px inserted above main layout.lockedTop = -3200; layout.documentTop = 400; lock.release(); assert.deepEqual(scrollToCalls[0], [0, 3600]); }); });
it("restores a locked scrollable ancestor's overflow on unlock", () => { const scrollable = document.createElement("div"); scrollable.style.overflowY = "auto"; Object.defineProperty(scrollable, "scrollHeight", { value: 200 }); Object.defineProperty(scrollable, "clientHeight", { value: 100 }); const target = document.createElement("div"); scrollable.appendChild(target); container.appendChild(scrollable);
const lock = createLock(target); assert.deepEqual(scrollable.style.overflow, "hidden"); lock.release(); assert.deepEqual(scrollable.style.overflow, ""); });});