import { describe, expect, it } from "vitest"; import { captureAnchor, restoreAnchor } from "./scroll-anchor"; // Three sections stacked on a page: 0-1000, 1000-1500, 1500-3500. const english = [ { top: 0, height: 1000 }, { top: 1000, height: 500 }, { top: 1500, height: 2000 }, ]; // The same sections in a language with longer texts. const german = [ { top: 0, height: 1200 }, { top: 1200, height: 800 }, { top: 2000, height: 2400 }, ]; describe("captureAnchor", () => { it("names the section the reading position is in and how far into it", () => { expect(captureAnchor(english, 0)).toEqual({ index: 0, ratio: 0 }); expect(captureAnchor(english, 250)).toEqual({ index: 0, ratio: 0.25 }); expect(captureAnchor(english, 1250)).toEqual({ index: 1, ratio: 0.5 }); expect(captureAnchor(english, 3000)).toEqual({ index: 2, ratio: 0.75 }); }); it("counts a section boundary as the start of the next section", () => { expect(captureAnchor(english, 1000)).toEqual({ index: 1, ratio: 0 }); expect(captureAnchor(english, 1500)).toEqual({ index: 2, ratio: 0 }); }); it("holds on to the last section below the end of the page", () => { expect(captureAnchor(english, 3500)).toEqual({ index: 2, ratio: 1 }); expect(captureAnchor(english, 9000)).toEqual({ index: 2, ratio: 1 }); }); // Above the first section there is only the header: the new page starts at its top too. it("has no anchor above the first section", () => { const offset = [{ top: 80, height: 600 }]; expect(captureAnchor(offset, 0)).toBeNull(); expect(captureAnchor(offset, 79)).toBeNull(); expect(captureAnchor(offset, 80)).toEqual({ index: 0, ratio: 0 }); expect(restoreAnchor(captureAnchor(offset, 0), offset)).toBe(0); }); it("takes the nearest section above when the position falls into a gap", () => { const gapped = [ { top: 0, height: 100 }, { top: 300, height: 100 }, ]; expect(captureAnchor(gapped, 200)).toEqual({ index: 0, ratio: 1 }); }); it("does not divide by the height of an empty section", () => { const empty = [ { top: 0, height: 0 }, { top: 0, height: 0 }, ]; expect(captureAnchor(empty, 50)).toEqual({ index: 1, ratio: 0 }); }); it("has no anchor on a page without sections", () => { expect(captureAnchor([], 120)).toBeNull(); }); }); describe("restoreAnchor", () => { it("finds the same place in a page whose sections have other heights", () => { expect(restoreAnchor({ index: 0, ratio: 0.25 }, german)).toBe(300); expect(restoreAnchor({ index: 1, ratio: 0.5 }, german)).toBe(1600); expect(restoreAnchor({ index: 2, ratio: 0.75 }, german)).toBe(3800); }); it("returns a whole pixel", () => { expect(restoreAnchor({ index: 1, ratio: 1 / 3 }, german)).toBe(1467); }); it("goes to the top when there is no anchor or the section is gone", () => { expect(restoreAnchor(null, german)).toBe(0); expect(restoreAnchor({ index: 3, ratio: 0.5 }, german)).toBe(0); expect(restoreAnchor({ index: 0, ratio: 0.5 }, [])).toBe(0); }); it("round-trips within the same page", () => { for (const y of [0, 10, 999, 1000, 1499, 2750]) { expect(restoreAnchor(captureAnchor(english, y), english)).toBe(y); } }); });