Files
learnharness/tests/plan-window.test.tsx
T
amosblomqvist 93033fac77 Plan placement (owner): the plan as marginalia — dock band's right flank + top-bar chip
Retires the fixed bottom-left plan window (which sat ON the sidebar and
bled into the session). Two placements replace it, per the owner's
lock-in:

- The plan chip in the top bar beside the phase chip — session-level
  structure beside session state; opens the full-DAG popup; present at
  every width; the access point.
- The plan marginalia: the BARE diagram (no card, no border, no shadow)
  in the dock band's right flank — marginalia on the lesson's page, not a
  floating widget. The band's reserved height (tall while a quiz is
  open) is its canvas; meet-fitted (PlanDiagram's new fill mode) so it
  fits both the flank's width and the band's height; live statuses +
  the yellow current-node glow; click opens the popup. Mounted only
  while a question owns the band — composer mode keeps the reading
  view pristine. Folding the dock fades + blurs it out of existence
  (300ms), returning to the bottom fades it back in. Rendered only when
  the flank holds a real diagram (>= 240px); narrower windows keep the
  chip alone.

Files: chat.tsx (chip, DockOverlay marginalia wrapper + flank measure,
PlanWindow removed), plan-window.tsx (PlanWindow → PlanMarginalia),
plan-block.tsx (PlanDiagram fill prop: h-full w-full meet-fit),
tests/plan-window.test.tsx (rewritten for the marginalia).
2026-09-14 11:23:28 +03:00

78 lines
3.0 KiB
TypeScript

import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { PlanMarginalia } from "../components/plan-window.tsx";
import { blockLayout, type PopupMessage } from "../components/plan-block.tsx";
import type { PlanTree } from "../src/core/contracts.ts";
// The plan marginalia: the bare diagram in the dock band's right flank —
// full plan (padded viewBox, meet-fit), statuses + glow live by derivation,
// nothing until a tree exists. Positioning and fade live in DockOverlay.
const CHAIN: PlanTree = {
nodes: [
{ id: "a", title: "Alpha" },
{ id: "b", title: "Beta" },
{ id: "c", title: "Gamma" },
],
edges: [
{ from: "a", to: "b" },
{ from: "b", to: "c" },
],
version: 1,
};
/** A transcript with a successful plan_init (carrying `tree`) and
* optionally a tagged quiz call. */
function transcript(tree: PlanTree, taggedNodeId?: string): PopupMessage[] {
return [
{ id: "m1", role: "user", parts: [] } as never,
{
id: "m2",
role: "assistant",
parts: [{ type: "tool-plan_init", state: "output-available", output: { ok: true, tree } }],
} as never,
...(taggedNodeId
? [
{
id: "m3",
role: "assistant",
parts: [{ type: "tool-quiz", state: "input-available", input: { nodeId: taggedNodeId } }],
} as never,
]
: []),
];
}
describe("PlanMarginalia render", () => {
it("renders nothing until a tree exists", () => {
assert.equal(renderToStaticMarkup(<PlanMarginalia messages={[]} />), "");
});
it("renders the FULL plan (padded full-extent viewBox), not a windowed crop", () => {
const html = renderToStaticMarkup(<PlanMarginalia messages={transcript(CHAIN)} />);
// The viewBox covers the whole layout plus the glow-clearing pad —
// never the retired window's 2:1 crop.
const layout = blockLayout(CHAIN.nodes, CHAIN.edges);
const pad = 24; // MARGIN_PAD: GRAPH_PAD + 16 (glow halo clearance)
const vb = `viewBox="${-pad} ${-pad} ${layout.width + 2 * pad} ${layout.height + 2 * pad}"`;
assert.ok(html.includes(vb), `expected ${vb} in the svg`);
assert.ok(!/viewBox="-?\d+(\.\d+)? -?\d+(\.\d+)? 360 180"/.test(html), "the windowed viewBox is retired");
// Meet-fit mode: the svg fills the band box (h-full w-full), letting
// preserveAspectRatio contain the diagram in the flank.
assert.ok(html.includes('class="block h-full w-full'), "fill mode classes");
});
it("glows the current node and dims the not-yet-visited (live statuses)", () => {
const html = renderToStaticMarkup(<PlanMarginalia messages={transcript(CHAIN, "b")} />);
// The yellow blurred halo marks the tagged check's node…
assert.ok(html.includes("fill-yellow-400/25"), "glow on the current node");
assert.ok(html.includes('data-testid="plan-node-b" data-status="current"'), "b is current");
// …and arrows into upcoming nodes carry the dimmed head.
assert.ok(
html.includes("url(#:r0:-dim)") || /url\(#\S*-dim\)/.test(html),
"dimmed arrowhead into the upcoming node",
);
});
});