import { describe, expect, it } from "vitest"; import { onScreen, place, roomFor, viewport } from "../src/lib/place"; import type { Box } from "../src/lib/place"; const VIEW = { width: 1280, height: 800 }; const PANEL = { width: 320, height: 200 }; const row = (over: Partial = {}): Box => ({ top: 300, left: 400, width: 500, height: 40, ...over, }); describe("onScreen", () => { it("skips the copy that is hidden and has no box", () => { const hidden = row({ top: 0, left: 0, width: 0, height: 0 }); const shown = row(); expect(onScreen([hidden, shown])).toBe(shown); }); it("takes the first one when several are on screen", () => { const first = row(); expect(onScreen([first, row({ top: 500 })])).toBe(first); }); it("says nothing when every copy is hidden", () => { expect(onScreen([row({ width: 0, height: 0 })])).toBeNull(); expect(onScreen([])).toBeNull(); }); }); describe("place", () => { it("sits just under the anchor", () => { expect(place(row(), PANEL, VIEW)).toEqual({ top: 346, left: 400 }); }); it("flips above when there is no room below", () => { expect(place(row({ top: 700 }), PANEL, VIEW)).toEqual({ top: 494, left: 400 }); }); it("stays inside the window when there is room neither way", () => { const tall = { width: 320, height: 780 }; const spot = place(row({ top: 400 }), tall, VIEW); expect(spot.top).toBeGreaterThanOrEqual(10); expect(spot.top + tall.height).toBeLessThanOrEqual(VIEW.height); }); it("pulls back from the right edge", () => { expect(place(row({ left: 1200 }), PANEL, VIEW).left).toBe(950); }); it("never goes off the left edge", () => { expect(place(row({ left: -40 }), PANEL, VIEW).left).toBe(10); }); it("centres rather than hiding in the corner with nothing to anchor to", () => { expect(place(null, PANEL, VIEW)).toEqual({ top: 300, left: 480 }); }); }); // A phone's keyboard shrinks the visual viewport and leaves the window alone. describe("place against a keyboard", () => { const SHRUNK = { width: 390, height: 400, top: 0, left: 0 }; it("keeps the panel above the keyboard", () => { const spot = place(row({ top: 380, left: 10, width: 370 }), PANEL, SHRUNK); expect(spot.top + PANEL.height).toBeLessThanOrEqual(SHRUNK.height); }); it("measures from where the visible area starts", () => { const scrolled = { width: 390, height: 400, top: 120, left: 0 }; const spot = place(null, PANEL, scrolled); expect(spot.top).toBeGreaterThanOrEqual(scrolled.top); expect(spot.top + PANEL.height).toBeLessThanOrEqual( scrolled.top + scrolled.height ); }); it("comes back into view when the anchor itself is under the keyboard", () => { // The row the panel hangs off is at 650, the keyboard leaves 380: neither // above nor below the anchor is reachable, so the visible area wins. const spot = place(row({ top: 650, height: 40 }), PANEL, SHRUNK); expect(spot.top).toBeGreaterThanOrEqual(10); expect(spot.top + PANEL.height).toBeLessThanOrEqual(SHRUNK.height); }); }); describe("roomFor", () => { it("leaves the panel less height than the visible area", () => { expect(roomFor({ width: 390, height: 400 })).toBeLessThan(400); }); it("never collapses to nothing when the keyboard takes almost everything", () => { expect(roomFor({ width: 390, height: 60 })).toBeGreaterThan(100); }); }); // Obsidian's phone navbar is not part of the visual viewport - it floats over // it - so a panel sized to the viewport still ends up underneath it. describe("viewport minus the floating navbar", () => { function fakeWin(navbarTop: number | null, height = 780): Window { const bar = { getBoundingClientRect: () => ({ top: navbarTop ?? 0, bottom: height, height: height - (navbarTop ?? 0), left: 0, right: 390, }), offsetParent: {}, }; return { innerWidth: 390, innerHeight: height, visualViewport: { width: 390, height, offsetTop: 0, offsetLeft: 0 }, document: { querySelector: (selector: string) => selector === ".mobile-navbar" && navbarTop !== null ? bar : null, }, } as unknown as Window; } it("takes the navbar off the bottom", () => { expect(viewport(fakeWin(724)).height).toBe(724); }); it("leaves the height alone with no navbar in the way", () => { expect(viewport(fakeWin(null)).height).toBe(780); }); it("still leaves a usable strip if a bar somehow covers everything", () => { expect(viewport(fakeWin(0)).height).toBeGreaterThanOrEqual(120); }); });