import { test } from "node:test"; import assert from "node:assert/strict"; import { addCell, defaultLayout, findSection, hasFillRow, insertAt, isColumnsLayout, moveToCell, moveToRow, moveWithin, parseLayoutParam, reconcileLayout, removeCell, removeSection, sameLayout, serializeLayout, setCellFlow, setCellSpan, setColumnCount, setRowCount, setRowSize, type Layout, type SectionPos, } from "./placement"; import { WIDGET_DEFAULTS } from "./config"; import type { SectionFlags, SectionId } from "./sections"; // Run from the repo root with: // ./node_modules/.bin/tsx --test editor/app/widget/lib/placement.test.ts // // The point of most of these is the normalization rule: a layout arrives from a // URL or from localStorage and can disagree with the visibility flags, which // are authoritative. Getting that wrong means a link that renders nothing. // // The other half is BACK-COMPATIBILITY. `l=` predates rows entirely, and every // already-copied widget link carries one — so each `l=` case here asserts the // same decode the columns model gave, and that it re-serializes to the byte- // identical string rather than being upgraded to a `g=`. const flags = (over: Partial = {}): SectionFlags => ({ ...WIDGET_DEFAULTS, ...over, }); // A layout of plain stacked columns, the shape `l=` describes: one auto row, // one single-track cell per column. Lets the ported cases read as they did. const cols = (...columns: SectionId[][]): Layout => ({ cols: columns.length, rows: [ { size: "auto", cells: columns.map((sections) => ({ sections, span: 1, flow: "col" })), }, ], }); // The `l` (or `g`) a layout serializes to, for the round-trip assertions. const ser = (layout: Layout, config: SectionFlags = flags()) => serializeLayout(layout, config); // --------------------------------------------------------------------------- // Defaults // --------------------------------------------------------------------------- test("the default layout is one cell of the enabled sections, in render order", () => { assert.deepEqual(defaultLayout(flags()), cols(["disk", "workers", "jobs"])); assert.deepEqual(defaultLayout(flags({ workers: false })), cols(["disk", "jobs"])); // Order is the widget's original render order, not the order flags were set. assert.deepEqual( defaultLayout(flags({ actionable: true, lastSync: true })), cols(["lastSync", "disk", "workers", "jobs", "actionable"]), ); // One auto row, one track: the pre-rows behaviour exactly. assert.equal(defaultLayout(flags()).cols, 1); assert.equal(defaultLayout(flags()).rows.length, 1); assert.equal(hasFillRow(defaultLayout(flags())), false); }); test("no layout param means the default layout", () => { assert.deepEqual( parseLayoutParam(undefined, undefined, flags()), defaultLayout(flags()), ); assert.deepEqual(parseLayoutParam("", "", flags()), defaultLayout(flags())); }); // --------------------------------------------------------------------------- // `l=` — the pre-rows contract // --------------------------------------------------------------------------- test("an l= param is decoded into one auto row of columns", () => { assert.deepEqual( parseLayoutParam("wk.jobs-disk", undefined, flags()), cols(["workers", "jobs"], ["disk"]), ); }); test("an unknown code is dropped, not rendered and not fatal", () => { assert.deepEqual( parseLayoutParam("wk.nosuch.jobs-disk", undefined, flags()), cols(["workers", "jobs"], ["disk"]), ); }); test("a duplicated section is placed once, where it appears first", () => { assert.deepEqual( parseLayoutParam("wk.jobs-wk.disk", undefined, flags()), cols(["workers", "jobs"], ["disk"]), ); }); test("a section that is enabled but unlisted joins the last cell", () => { // `disk` is on by default and absent from the param: it must still render, or // a link baked before a section was switched on would silently lose it. assert.deepEqual( parseLayoutParam("wk-jobs", undefined, flags()), cols(["workers"], ["jobs", "disk"]), ); }); test("a section named in the layout but switched off is dropped", () => { assert.deepEqual( parseLayoutParam("wk.jobs-disk", undefined, flags({ disk: false })), cols(["workers", "jobs"], []), ); }); test("a layout that survives nothing falls back to the default", () => { assert.deepEqual( parseLayoutParam("nope.alsonope", undefined, flags()), defaultLayout(flags()), ); }); test("more columns than the maximum are merged into the last", () => { // Six tracks is the cap now, so a seven-column link merges the tail rather // than being rejected. const on = flags({ cleanable: true, actionable: true, cleanChannels: true, lastSync: true }); assert.deepEqual( parseLayoutParam("wk-jobs-disk-cln-act-clnl-lsync", undefined, on), cols( ["workers"], ["jobs"], ["disk"], ["cleanable"], ["actionable"], ["cleanChannels", "lastSync"], ), ); }); test("serializing a default layout gives nothing at all", () => { // This is what keeps an all-default config serializing to a bare /widget. assert.deepEqual(ser(defaultLayout(flags())), {}); const off = flags({ workers: false }); assert.deepEqual(ser(defaultLayout(off), off), {}); }); test("every columns layout still round-trips through l=, byte-identical", () => { for (const raw of [ "wk.jobs-disk", "disk.jobs.wk", "wk.jobs.disk-", "disk-wk-jobs", ]) { const layout = parseLayoutParam(raw, undefined, flags()); assert.deepEqual(ser(layout), { l: raw }, `round-trip of ${raw}`); // ...and never as a g=, which no older client would understand. assert.equal(ser(layout).g, undefined); } }); test("an empty trailing column round-trips", () => { const layout = cols(["workers", "jobs", "disk"], []); assert.deepEqual(ser(layout), { l: "wk.jobs.disk-" }); assert.deepEqual(parseLayoutParam("wk.jobs.disk-", undefined, flags()), layout); }); // --------------------------------------------------------------------------- // `g=` — rows, spans, flow, fill // --------------------------------------------------------------------------- // The Cockpit arrangement: controls across the top, a dense rail of totals under // it, then a fill row with the scheduler beside the two scrolling lists. const COCKPIT = "ctl*2_lsync.disk.cln.bf*2r_sched*f-act.clnl"; const cockpitFlags = flags({ controls: true, lastSync: true, scheduler: true, cleanable: true, backfill: true, actionable: true, cleanChannels: true, jobs: false, workers: false, disk: true, }); test("g= decodes rows, spans, flow and the fill marker", () => { const layout = parseLayoutParam(undefined, COCKPIT, cockpitFlags); assert.deepEqual(layout, { cols: 2, rows: [ { size: "auto", cells: [{ sections: ["controls"], span: 2, flow: "col" }], }, { size: "auto", cells: [ { sections: ["lastSync", "disk", "cleanable", "backfill"], span: 2, flow: "row", }, ], }, { size: "fill", cells: [ { sections: ["scheduler"], span: 1, flow: "col" }, { sections: ["actionable", "cleanChannels"], span: 1, flow: "col" }, ], }, ], }); assert.ok(hasFillRow(layout)); assert.ok(!isColumnsLayout(layout)); }); test("g= re-serializes byte-identically", () => { const layout = parseLayoutParam(undefined, COCKPIT, cockpitFlags); assert.deepEqual(ser(layout, cockpitFlags), { g: COCKPIT }); // ...and never alongside an l=, which would be two contradicting layouts. assert.equal(ser(layout, cockpitFlags).l, undefined); }); test("g= wins over l= when both are present", () => { const layout = parseLayoutParam("wk.jobs-disk", COCKPIT, cockpitFlags); assert.equal(layout.rows.length, 3); }); test("the fill marker survives on any cell, and normalizes onto the first", () => { // A hand-edited link that marks the SECOND cell still fills the row, and // re-serializes with the marker on the first cell. const layout = parseLayoutParam(undefined, "wk-jobs*f", flags()); assert.equal(layout.rows[0].size, "fill"); assert.deepEqual(ser(layout), { g: "wk*f-jobs.disk" }); }); test("an out-of-range span is clamped, and the row still sums to the tracks", () => { const layout = parseLayoutParam(undefined, "wk*9-jobs", flags()); const row = layout.rows[0]; assert.equal( row.cells.reduce((s, c) => s + c.span, 0), layout.cols, ); assert.ok(row.cells.every((c) => c.span >= 1 && c.span <= layout.cols)); }); test("an unknown modifier letter is ignored rather than fatal", () => { assert.deepEqual( parseLayoutParam(undefined, "wk*2z-jobs.disk", flags()), { cols: 3, rows: [ { size: "auto", cells: [ { sections: ["workers"], span: 2, flow: "col" }, { sections: ["jobs", "disk"], span: 1, flow: "col" }, ], }, ], }, ); }); test("more rows than the maximum are merged into the last", () => { const layout = parseLayoutParam( undefined, "wk_jobs_disk_cln_act_clnl_lsync", flags({ cleanable: true, actionable: true, cleanChannels: true, lastSync: true }), ); assert.equal(layout.rows.length, 6); assert.deepEqual(layout.rows[5].cells[0].sections, ["cleanChannels", "lastSync"]); }); test("a g= that survives nothing falls back to the default", () => { assert.deepEqual( parseLayoutParam(undefined, "nope_alsonope", flags()), defaultLayout(flags()), ); }); // --------------------------------------------------------------------------- // Reconcile // --------------------------------------------------------------------------- test("reconcile drops what is off and appends to the last cell of the last row", () => { assert.deepEqual( reconcileLayout(cols(["workers", "jobs"], ["disk"]), flags({ disk: false })), cols(["workers", "jobs"], []), ); assert.deepEqual( reconcileLayout(cols(["workers"], ["jobs"]), flags({ actionable: true })), cols(["workers"], ["jobs", "disk", "actionable"]), ); // Multi-row: the newly enabled section lands in the LAST cell of the LAST // row, not back at the top where it would push the arrangement around. const grid = parseLayoutParam(undefined, "wk_jobs-disk", flags()); const next = reconcileLayout(grid, flags({ actionable: true })); assert.deepEqual(next.rows[1].cells[1].sections, ["disk", "actionable"]); }); // --------------------------------------------------------------------------- // Movement // --------------------------------------------------------------------------- test("moveWithin swaps with the neighbour and stops at the bounds", () => { const layout = cols(["workers", "jobs", "disk"]); assert.deepEqual( moveWithin(layout, 0, 0, 2, -1), cols(["workers", "disk", "jobs"]), ); assert.equal(moveWithin(layout, 0, 0, 0, -1), layout); assert.equal(moveWithin(layout, 0, 0, 2, 1), layout); }); test("moveToCell lands at the same height, or the end of a shorter cell", () => { assert.deepEqual( moveToCell(cols(["workers", "jobs"], ["disk"]), 0, 0, 1, 1), cols(["workers"], ["disk", "jobs"]), ); assert.deepEqual( moveToCell(cols(["workers", "jobs", "disk"], []), 0, 0, 2, 1), cols(["workers", "jobs"], ["disk"]), ); // No neighbouring cell → unchanged. const one = cols(["workers", "jobs"]); assert.equal(moveToCell(one, 0, 0, 0, 1), one); }); test("moveToRow carries a section into the row above or below", () => { const layout = parseLayoutParam(undefined, "wk.jobs_disk", flags()); const down = moveToRow(layout, 0, 0, 0, 1); assert.deepEqual(down.rows[0].cells[0].sections, ["jobs"]); assert.deepEqual(down.rows[1].cells[0].sections, ["disk", "workers"]); // Off the end → unchanged. assert.equal(moveToRow(layout, 0, 0, 0, -1), layout); }); test("insertAt accounts for the removal when moving down within a cell", () => { // Dragging `workers` (index 0) to the slot below `jobs` (index 2 as drawn) // must land it after jobs, not before. assert.deepEqual( insertAt(cols(["workers", "jobs", "disk"]), "workers", 0, 0, 2), cols(["jobs", "workers", "disk"]), ); assert.deepEqual( insertAt(cols(["workers", "jobs"], ["disk"]), "disk", 0, 0, 0), cols(["disk", "workers", "jobs"], []), ); // Across rows: pulled out of wherever it was, wherever that was. const grid = parseLayoutParam(undefined, "wk.jobs_disk", flags()); const moved = insertAt(grid, "disk", 0, 0, 0); assert.deepEqual(moved.rows[0].cells[0].sections, ["disk", "workers", "jobs"]); assert.deepEqual(moved.rows[1].cells[0].sections, []); }); test("removeSection and sameLayout", () => { assert.deepEqual( removeSection(cols(["workers", "jobs"], ["disk"]), "jobs"), cols(["workers"], ["disk"]), ); assert.ok(sameLayout(cols(["workers"]), cols(["workers"]))); assert.ok(!sameLayout(cols(["workers"]), cols(["workers"], []))); assert.ok(!sameLayout(cols(["workers", "jobs"]), cols(["jobs", "workers"]))); // Structure counts too, not just the section order. const filled = setRowSize(cols(["workers"]), 0, "fill"); assert.ok(!sameLayout(filled, cols(["workers"]))); }); // --------------------------------------------------------------------------- // Structure // --------------------------------------------------------------------------- test("setColumnCount merges trailing cells rather than dropping them", () => { assert.deepEqual( setColumnCount(cols(["workers"], ["jobs"], ["disk"]), 2), cols(["workers"], ["jobs", "disk"]), ); assert.deepEqual( setColumnCount(cols(["workers", "jobs"]), 3), cols(["workers", "jobs"], [], []), ); }); test("setColumnCount keeps every row summing to the new track count", () => { const grid = parseLayoutParam(undefined, "wk*2_jobs-disk", flags()); for (const n of [1, 2, 3, 4, 5, 6]) { const next = setColumnCount(grid, n); assert.equal(next.cols, n); for (const row of next.rows) { assert.equal( row.cells.reduce((s, c) => s + c.span, 0), n, `row sums to ${n}`, ); } } }); test("setRowCount merges trailing rows rather than dropping them", () => { const three = parseLayoutParam(undefined, "wk_jobs_disk", flags()); const two = setRowCount(three, 2); assert.equal(two.rows.length, 2); assert.deepEqual(two.rows[1].cells[0].sections, ["jobs", "disk"]); // Growing appends an empty row, so there is somewhere to drop things. const four = setRowCount(three, 4); assert.equal(four.rows.length, 4); assert.deepEqual(four.rows[3].cells[0].sections, []); }); test("setCellSpan takes the tracks from the neighbours, merging what it swallows", () => { const grid = parseLayoutParam(undefined, "wk-jobs-disk", flags()); assert.equal(grid.cols, 3); // Widening by one swallows the cell beside it — its sections move in rather // than being dropped — and the row still sums to the track count. const wide = setCellSpan(grid, 0, 0, 2); assert.deepEqual( wide.rows[0].cells.map((c) => c.span), [2, 1], ); assert.deepEqual(wide.rows[0].cells[0].sections, ["workers", "jobs"]); assert.deepEqual(wide.rows[0].cells[1].sections, ["disk"]); // Asking for the whole row gives the whole row, everything merged in order. const max = setCellSpan(grid, 0, 0, 6); assert.equal(max.rows[0].cells.length, 1); assert.equal(max.rows[0].cells[0].span, 3); assert.deepEqual(max.rows[0].cells[0].sections, ["workers", "jobs", "disk"]); // Narrowing the last cell in a row frees its tracks into a new empty cell — // the exact inverse, so a widen is undoable. const back = setCellSpan(max, 0, 0, 1); assert.deepEqual( back.rows[0].cells.map((c) => c.span), [1, 2], ); assert.deepEqual(back.rows[0].cells[1].sections, []); }); test("setCellFlow, setRowSize and hasFillRow", () => { const grid = parseLayoutParam(undefined, "lsync.disk-jobs", flags({ lastSync: true })); const rail = setCellFlow(grid, 0, 0, "row"); assert.equal(rail.rows[0].cells[0].flow, "row"); assert.equal(setCellFlow(rail, 0, 0, "row"), rail); assert.ok(!hasFillRow(rail)); const filled = setRowSize(rail, 0, "fill"); assert.ok(hasFillRow(filled)); assert.equal(setRowSize(filled, 0, "fill"), filled); }); test("addCell splits a row, removeCell merges one back", () => { const one = parseLayoutParam(undefined, "wk.jobs.disk", flags()); const two = addCell(one, 0); assert.equal(two.rows[0].cells.length, 1, "a one-track row has no room"); const wide = setColumnCount(one, 2); assert.equal(wide.rows[0].cells.length, 2); const three = addCell(setColumnCount(one, 3), 0); assert.equal(three.rows[0].cells.length, 3); const merged = removeCell(wide, 0, 1); assert.equal(merged.rows[0].cells.length, 1); assert.deepEqual(merged.rows[0].cells[0].sections, ["workers", "jobs", "disk"]); // The last cell of a row can't be removed — a row always has one. assert.equal(removeCell(merged, 0, 0), merged); }); test("the board's round trip: add a row, fill it, widen a cell, and back", () => { // The exact sequence the builder e2e drives, asserted on the params it writes. let layout = parseLayoutParam(undefined, undefined, flags()); layout = setColumnCount(layout, 2); assert.deepEqual(ser(layout), { l: "disk.wk.jobs-" }); layout = setRowCount(layout, 2); layout = insertAt(layout, "jobs", 1, 0, 0); assert.deepEqual(ser(layout), { g: "disk.wk-_jobs-" }); layout = setRowSize(layout, 1, "fill"); assert.deepEqual(ser(layout), { g: "disk.wk-_jobs*f-" }); layout = setCellSpan(layout, 0, 0, 2); assert.deepEqual(ser(layout), { g: "disk.wk*2_jobs*f-" }); // Narrowing it back and dropping the extra row falls all the way back to `l=`. layout = setCellSpan(layout, 0, 0, 1); layout = setRowSize(layout, 1, "auto"); layout = setRowCount(layout, 1); assert.deepEqual(ser(layout), { l: "disk.wk-jobs" }); }); test("findSection reports the row, cell and index", () => { const grid = parseLayoutParam(undefined, "wk.jobs_disk", flags()); const at: SectionPos = { row: 0, cell: 0, index: 1 }; assert.deepEqual(findSection(grid, "jobs"), at); assert.deepEqual(findSection(grid, "disk"), { row: 1, cell: 0, index: 0 }); assert.equal(findSection(grid, "cleanable"), null); });