Files
beaver-calendar/tests/place.test.ts
T
hh f980abcc1d fix(view,place): the boards view pads clear of the phone's navbar
The panel had an inset for this; the calendar never did. Its root filled the
leaf, and Obsidian's phone navbar floats over a leaf rather than shortening
it, so the scroll container ran to the bottom of the screen and the last row
of the list sat under the bar - unreachable.

`lib/inset.ts` is the same pure measurement the panel plugin grew, with the
same tests: the deeper of navbar and keyboard, never their sum, and a bar that
sits beside the view covers nothing of it. The view re-measures by frame until
it holds still and writes `--bcal-inset-bottom`.

`viewport()` subtracts the bar too, so anchored panels and the quick look are
placed against what is actually reachable instead of ending under it.
2026-09-05 01:05:14 +02:00

136 lines
4.5 KiB
TypeScript

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> = {}): 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);
});
});