Ctrl K
ComunidadGuías…

tests/paneWidths.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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), {});
  });
});