tests/paneWidths.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { describe, it } from "node:test";
import {
GRIP_HIT,
MAIN_MIN,
PANE_MAX,
PANE_MIN,
clampPane,
dragWidth,
onScrollbar,
layoutPanes,
paneStyle,
parsePaneWidths,
} from "../client/paneWidths.ts";
describe("pane widths", () => {
it("clamps into the range a pane can be", () => {
assert.equal(clampPane(10), PANE_MIN);
assert.equal(clampPane(9000), PANE_MAX);
assert.equal(clampPane(300.4), 300);
});
// THE DRAG IS RELATIVE. The old form was `pointerX - rect.left`, so the pane
// jumped to the pointer's own offset from the pane's edge the instant the
// pointer moved: take hold at the inner end of the strip and the pane
// snapped several pixels narrower before it began to track the hand.
it("moves a pane by how far the hand moved, from wherever it took hold", () => {
// Grabbed at x=288 on a 292px sidebar; one pixel right is one pixel wider.
assert.equal(dragWidth(289, 288, 292, true), 293);
assert.equal(dragWidth(288, 288, 292, true), 292);
assert.equal(dragWidth(238, 288, 292, true), 242);
// The trailing pane grows as the pointer moves back toward the start.
assert.equal(dragWidth(1139, 1140, 300, false), 301);
assert.equal(dragWidth(1140, 1140, 300, false), 300);
});
it("leaves the width byte-identical for a grab with no move", () => {
for (const x of [280, 286, 292, 298]) {
assert.equal(dragWidth(x, x, 292, true), 292);
assert.equal(dragWidth(x, x, 300, false), 300);
}
});
// The strip's width is a number in TWO files — this one and app.css — and
// the whole point of it is arithmetic: 12px wide, started half its width less
// half the divider before the seam, so the hairline the eye aims at is the
// middle of the hit area. A stylesheet that drifts from GRIP_HIT is a grip
// that is no longer centred on anything, which is the defect it exists for.
describe("the strip the divider sits in the middle of", () => {
// app.css and, since the sweep split it, the grips' own sheet — read in
// the order index.html links them.
const css =
readFileSync(new URL("../client/styles/app.css", import.meta.url), "utf8") +
readFileSync(new URL("../client/styles/grips.css", import.meta.url), "utf8");
const inset = GRIP_HIT / 2 - 0.5;
it("is the same 12px in the stylesheet", () => {
assert.equal(GRIP_HIT, 12);
assert.match(css, new RegExp(`\\.s-pane-grip \\{[^}]*width: ${GRIP_HIT}px;`), "app.css must draw the strip GRIP_HIT wide");
});
it("starts half a strip less half a divider before each pane's seam", () => {
for (const pane of ["sidebar", "panel"] as const) {
const token = pane === "sidebar" ? "--sidebar-w" : "--panel-w";
const wanted = `calc(var(${token}) - ${inset}px)`;
assert.ok(css.includes(wanted), `app.css should position the ${pane} grip at ${wanted}`);
}
});
it("is a child of the shell, hidden by the shell's own fold classes", () => {
// Hoisted out of the panes (they are `overflow: hidden`, so a strip that
// reaches past the divider would be clipped), so the collapse tests
// cannot be descendant selectors on the pane any more.
assert.ok(!/\.s-sidebar--collapsed \.s-pane-grip/.test(css), "a hoisted grip cannot be hidden by the pane's own class");
assert.ok(css.includes(".s-app--nosidebar .s-pane-grip--sidebar"), "the shell's fold class hides the sidebar's grip");
assert.ok(css.includes(".s-app--nopanel .s-pane-grip--panel"), "the shell's fold class hides the panel's grip");
});
it("keeps the two panes' tokens meaning the same thing", () => {
// Both panes are `var(--pane-w) + 1px` of border box, so ONE arithmetic
// serves both grips and `clientWidth` is the number the drag writes.
assert.match(css, /\.s-sidebar \{[^}]*width: calc\(var\(--sidebar-w\) \+ 1px\);/);
assert.match(css, /\.s-panel \{[^}]*width: calc\(var\(--panel-w\) \+ 1px\);/);
});
});
// The half of the strip that lies over the pane shares its pixels with the
// tree's scrollbar wherever the platform draws a CLASSIC one — Windows. A
// press there is a scroll; a press past the divider is a resize. (Measured
// under Wine on the real Windows renderer: the tree's bar is 18 CSS px, so
// the strip's inner 5.5px cover its outer third and leave the rest.)
describe("a press on a real scrollbar", () => {
const tree = { left: 0, right: 292 };
it("is a scroll where the bar takes room in the layout", () => {
assert.equal(onScrollbar(290, tree, 18, false), true, "inside the bar");
assert.equal(onScrollbar(275, tree, 18, false), true, "the bar's inner edge");
assert.equal(onScrollbar(270, tree, 18, false), false, "inboard of the bar is content");
assert.equal(onScrollbar(295, tree, 18, false), false, "past the pane is the note");
});
it("is nothing at all where the bar is an overlay", () => {
// macOS, Linux, Android: no bar in the layout, so the grip keeps the
// whole strip rather than losing a third of it to an invisible band.
for (const x of [270, 286, 290, 292]) assert.equal(onScrollbar(x, tree, 0, false), false);
});
it("follows the reading direction, because the bar does", () => {
// In Arabic a vertical scrollbar is on the scroller's LEFT.
assert.equal(onScrollbar(6, tree, 18, true), true);
assert.equal(onScrollbar(30, tree, 18, true), false);
assert.equal(onScrollbar(290, tree, 18, true), false);
});
});
const room = (viewport: number, sidebarDocked = true, panelDocked = true) => ({ viewport, sidebarDocked, panelDocked });
describe("the room the window actually has", () => {
it("leaves both panes alone when they fit", () => {
assert.deepEqual(layoutPanes({ sidebar: 292, panel: 300 }, room(1440)), { sidebar: 292, panel: 300 });
});
it("never lets two dragged panes swallow the note", () => {
const fits = layoutPanes({ sidebar: 560, panel: 560 }, room(904));
assert.ok(fits.sidebar! >= PANE_MIN, `sidebar ${fits.sidebar}`);
assert.ok(fits.panel! >= PANE_MIN, `panel ${fits.panel}`);
assert.ok(904 - fits.sidebar! - fits.panel! - 2 >= MAIN_MIN, "the note keeps its floor");
});
it("takes the shortfall out of the pane that is NOT under the hand first", () => {
const fits = layoutPanes({ sidebar: 560, panel: 560 }, room(1200), "sidebar");
assert.equal(fits.sidebar, 560);
assert.equal(fits.panel, 1200 - 560 - 2 - MAIN_MIN);
});
it("gives a collapsed pane's room to the other", () => {
assert.deepEqual(layoutPanes({ sidebar: 560, panel: 560 }, room(904, true, false)), { sidebar: 560 });
});
it("says nothing about a pane it has no reason to constrain", () => {
// Nothing stored and room to spare: the stylesheet's own clamp must win
// (`--sidebar-w: clamp(224px, calc(100vw - 776px), 292px)` is the rule
// that hands the 1000–1068 band its surplus to the reading column).
assert.deepEqual(paneStyle({}, room(1440)), { sidebar: null, panel: null });
// Nothing stored, but no room: the property has something to say.
assert.ok(paneStyle({}, room(820)).panel! < 300);
// Stored: always said, so a reload opens at the width that was dragged.
assert.equal(paneStyle({ sidebar: 380 }, room(1440)).sidebar, 380);
});
});
// A WIDTH THE READER DID NOT ASK FOR DOES NOT ANIMATE — AND THE FLAG THAT
// SAYS SO DOES NOT OUTLIVE IT. `paneStill` only ever went up: a window under
// NARROW_QUERY auto-collapses the outline panel at boot, so on a 1366 laptop
// the shell wore `s-app--pane-still` for the life of the page. Measured at
// 1300: `.s-sidebar`, `.s-panel` and `.s-main` all at transition-duration 0s
// at boot and still 0s after the reader's own Ctrl/Cmd+Alt+B — and on the
// phone, where the notes drawer slides on that same `.s-sidebar`, the drawer
// stopped moving at all (`transition-property: none`, left −330 → 0 in one
// frame).
describe("the still flag comes back down", () => {
// The pane setters are the store's preferences slice since the sweep split
// client/state.ts (client/state/prefsSlice.ts); the rule is the same.
const state = readFileSync(new URL("../client/state/prefsSlice.ts", import.meta.url), "utf8");
it("is raised only by the viewport's own collapse", () => {
assert.match(state, /collapsePanelForViewport: \(panelCollapsed\) => set\(\{ panelCollapsed, paneStill: true \}\)/);
});
// Every setter that moves a pane the reader is touching. Each lowers the
// flag in the SAME `set` that starts its own animation, so the class is
// gone in the commit the transition needs — no timer, no effect, nothing
// that could land a frame late.
// (setSidebarOpen, the phone drawer's, went with the drawer in 3.27.0.)
for (const setter of ["setPanelCollapsed", "setSidebarCollapsed", "setZen"]) {
it(`${setter} lowers it in the same commit`, () => {
const at = state.indexOf(`${setter}: (`);
assert.ok(at > 0, `${setter} should exist`);
const body = state.slice(at, state.indexOf("\n },", at) + 1);
assert.ok(body.includes("paneStill: false"), `${setter} must hand the 0.18s back to the reader`);
});
}
});
// A width the WINDOW asked for arrives at once. Before this the resize
// listener wrote the clamped widths through the panes' own 0.18s: at
// 1440 → 904 with {560, 560} stored, `.s-main` measured 0px at 30ms and
// 274px at 110ms before reaching its 320px floor — the note losing its
// column on every resize, which is the one thing MAIN_MIN is for.
it("re-clamps a resized window without the 0.18s", () => {
const grip = readFileSync(new URL("../client/components/PaneGrip.tsx", import.meta.url), "utf8");
const at = grip.indexOf("const apply = (): void =>");
assert.ok(at > 0, "usePaneLayout should still have its rAF-throttled apply");
assert.match(grip.slice(at, at + 600), /applyPaneWidthsNow\(document\.documentElement, paneRoom\(\)\)/);
// The double-click reset is the hand, and keeps the animation.
assert.match(grip, /applyPaneWidths\(document\.documentElement, paneRoom\(\)\)/);
});
it("reads stored widths and forgets junk", () => {
assert.deepEqual(parsePaneWidths('{"sidebar":260,"panel":"x","other":1}'), { sidebar: 260 });
assert.deepEqual(parsePaneWidths("not json"), {});
assert.deepEqual(parsePaneWidths(null), {});
});
});