8747074e52
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
194 lines
7.0 KiB
JavaScript
194 lines
7.0 KiB
JavaScript
// What a node in a delegated surface MEANS, and what the page bar reads as.
|
|
// Both are decisions, so neither needs a browser — context/code/clients.md.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
cellAction,
|
|
cellAttrs,
|
|
gridPlan,
|
|
indexAction,
|
|
menuKey,
|
|
pageEntries,
|
|
pageItemAction,
|
|
} from "../shell.js";
|
|
|
|
const cell = (text, extra = {}) => ({
|
|
text,
|
|
numeric: true,
|
|
error: false,
|
|
selected: false,
|
|
editable: true,
|
|
...extra,
|
|
});
|
|
|
|
/// The shape the row index has to survive: a group header between data rows,
|
|
/// which is exactly what walking the table back up would get wrong.
|
|
function grouped() {
|
|
return {
|
|
row_levels: 1,
|
|
col_levels: [["Revenue", "Cost"]],
|
|
col_groups: [],
|
|
totals: [],
|
|
editing: null,
|
|
rows: [
|
|
{ kind: "group", label: "Hot", collapsed: false },
|
|
{ kind: "data", labels: ["Espresso"], cells: [cell("1"), cell("2")] },
|
|
{ kind: "group", label: "Cold", collapsed: true },
|
|
{
|
|
kind: "data",
|
|
labels: ["Cold Brew"],
|
|
cells: [cell("3", { editable: false }), cell("4")],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
test("a cell's attributes carry the plan's answers back to the handler", () => {
|
|
const plan = gridPlan(grouped());
|
|
const last = plan.body.at(-1).cells[1];
|
|
const action = cellAction(cellAttrs(last), last.editable);
|
|
assert.deepEqual(action, { row: 1, col: 1, editable: true });
|
|
});
|
|
|
|
// improvise-b8ot: the DATA row index is what a click sends, and `gridPlan`
|
|
// counts it because a group header is not a data row. A delegated handler
|
|
// that inferred the row from the node's position in the table would say 3.
|
|
test("the row a delegated click means is the data index, not the table row", () => {
|
|
const plan = gridPlan(grouped());
|
|
const rows = plan.body
|
|
.filter((r) => r.kind === "data")
|
|
.map((r) => cellAction(cellAttrs(r.cells[0]), r.cells[0].editable).row);
|
|
assert.deepEqual(rows, [0, 1]);
|
|
});
|
|
|
|
test("editable is the class, so an unsettable cell opens no editor", () => {
|
|
const plan = gridPlan(grouped());
|
|
const aggregate = plan.body.at(-1).cells[0];
|
|
assert.equal(aggregate.classes.editable, false);
|
|
assert.equal(cellAction(cellAttrs(aggregate), false).editable, false);
|
|
});
|
|
|
|
// Every header, the corner and the totals strip reach the delegated handler
|
|
// too. None of them is a cell, and `Number("")` is 0 rather than nothing.
|
|
test("a node carrying no coordinates means nothing at all", () => {
|
|
assert.equal(cellAction({}, false), null);
|
|
assert.equal(cellAction({ row: "0" }, false), null);
|
|
assert.equal(cellAction({ row: "", col: "" }, false), null);
|
|
assert.equal(cellAction({ row: "x", col: "0" }, false), null);
|
|
assert.equal(cellAction({ row: "1.5", col: "0" }, false), null);
|
|
});
|
|
|
|
test("an index surface reads its row back, and nothing else", () => {
|
|
assert.equal(indexAction({ index: "0" }), 0);
|
|
assert.equal(indexAction({ index: "12" }), 12);
|
|
assert.equal(indexAction({}), null);
|
|
assert.equal(indexAction({ index: "" }), null);
|
|
assert.equal(indexAction({ index: "-1" }), null);
|
|
});
|
|
|
|
// improvise-p7lh: a grid filtered to Q1 said nothing at all, and `[`/`]`
|
|
// moved it in silence. The bar is what says which quarter this is.
|
|
test("the page bar names the category and the item it is fixed to", () => {
|
|
const [entry] = pageEntries({
|
|
page_coords: [
|
|
{
|
|
cat: "Quarter",
|
|
selected: "Q2",
|
|
cycle: { at: 1, count: 4, items: ["Q1", "Q2", "Q3", "Q4"] },
|
|
},
|
|
],
|
|
});
|
|
assert.equal(entry.text, "Quarter = Q2");
|
|
assert.equal(entry.position, "2/4", "one-based, as a user counts");
|
|
assert.equal(entry.navigable, true);
|
|
});
|
|
|
|
// Gotcha 27: Page is navigable and Filter is not. A Filter axis offers no
|
|
// position, because there is no key that would move it.
|
|
test("a filter axis is shown, and is not offered as something to cycle", () => {
|
|
const [entry] = pageEntries({
|
|
page_coords: [{ cat: "Region", selected: "North", cycle: null }],
|
|
});
|
|
assert.equal(entry.text, "Region = North");
|
|
assert.equal(entry.position, "");
|
|
assert.equal(entry.navigable, false);
|
|
});
|
|
|
|
test("no fixed categories is an empty bar, not a blank one", () => {
|
|
assert.deepEqual(pageEntries({ page_coords: [] }), []);
|
|
});
|
|
|
|
// improvise-rw6l: the arrow and the menu follow `cycle`, which is the Page /
|
|
// Filter split decided in Rust — never "is there an entry on the bar".
|
|
test("a page axis offers its items, and a filter axis offers none", () => {
|
|
const [page, filter] = pageEntries({
|
|
page_coords: [
|
|
{
|
|
cat: "Quarter",
|
|
selected: "Q3",
|
|
cycle: { at: 2, count: 3, items: ["Q1", "Q2", "Q3"] },
|
|
},
|
|
{ cat: "Region", selected: "North", cycle: null },
|
|
],
|
|
});
|
|
assert.deepEqual(page.items, [
|
|
{ item: 0, label: "Q1", selected: false },
|
|
{ item: 1, label: "Q2", selected: false },
|
|
{ item: 2, label: "Q3", selected: true },
|
|
]);
|
|
assert.equal(page.index, 0, "the axis names itself by its place on the bar");
|
|
assert.deepEqual(
|
|
filter.items,
|
|
[],
|
|
"a Filter axis constrains without being navigable: nothing to pick",
|
|
);
|
|
assert.equal(filter.index, 1);
|
|
});
|
|
|
|
// One meaning across two surfaces (improvise-b8ot): a chip carries an axis, a
|
|
// menu row carries an axis AND an item. Nothing infers from structure.
|
|
test("a chip opens the menu and a menu row picks, one attribute apart", () => {
|
|
assert.deepEqual(pageItemAction({ axis: "1" }), { kind: "open", axis: 1 });
|
|
assert.deepEqual(pageItemAction({ axis: "1", item: "0" }), {
|
|
kind: "pick",
|
|
axis: 1,
|
|
item: 0,
|
|
});
|
|
assert.equal(pageItemAction({}), null, "the gaps between the chips are not a chip");
|
|
assert.equal(pageItemAction({ axis: "" }), null);
|
|
assert.equal(pageItemAction({ axis: "-1" }), null);
|
|
assert.deepEqual(
|
|
pageItemAction({ axis: "0", item: "" }),
|
|
{ kind: "open", axis: 0 },
|
|
"an empty item attribute is NO item, where Number('') would say 0",
|
|
);
|
|
});
|
|
|
|
// The menu owns the keyboard while it is open, so every key is answered and
|
|
// nothing falls through to the grid it is anchored to.
|
|
test("the menu answers its own keys and swallows the rest", () => {
|
|
const at = (active) => ({ count: 3, active });
|
|
assert.deepEqual(menuKey("ArrowDown", at(0)), { kind: "move", item: 1 });
|
|
assert.deepEqual(menuKey("j", at(2)), { kind: "move", item: 0 }, "and it wraps");
|
|
assert.deepEqual(menuKey("ArrowUp", at(0)), { kind: "move", item: 2 }, "both ways");
|
|
assert.deepEqual(menuKey("k", at(1)), { kind: "move", item: 0 });
|
|
assert.deepEqual(menuKey("Home", at(2)), { kind: "move", item: 0 });
|
|
assert.deepEqual(menuKey("End", at(0)), { kind: "move", item: 2 });
|
|
assert.deepEqual(menuKey("Enter", at(1)), { kind: "pick", item: 1 });
|
|
assert.deepEqual(menuKey(" ", at(1)), { kind: "pick", item: 1 });
|
|
assert.deepEqual(menuKey("Escape", at(1)), { kind: "close" });
|
|
assert.deepEqual(menuKey("Tab", at(1)), { kind: "close" });
|
|
assert.deepEqual(
|
|
menuKey("h", at(1)),
|
|
{ kind: "ignore" },
|
|
"a grid key is SWALLOWED, not passed on — the grid is behind the menu",
|
|
);
|
|
assert.deepEqual(
|
|
menuKey("ArrowDown", { count: 0, active: 0 }),
|
|
{ kind: "close" },
|
|
"a menu with nothing in it cannot be moved around",
|
|
);
|
|
});
|