Limit and label task hierarchy
This commit is contained in:
+21
-4
@@ -415,7 +415,7 @@ test("enforces per-mutation and per-session task limits atomically", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts eight task levels and rejects deeper trees atomically", () => {
|
||||
test("allows three stored levels beneath the virtual root and rejects deeper task trees atomically", () => {
|
||||
const nested = (depth: number) => {
|
||||
let task: { name: string; children?: Array<{ name: string; children?: unknown[] }> } = { name: `Level ${depth}` };
|
||||
for (let level = depth - 1; level >= 1; level -= 1) {
|
||||
@@ -425,10 +425,27 @@ test("accepts eight task levels and rejects deeper trees atomically", () => {
|
||||
};
|
||||
|
||||
const state = new TaskState();
|
||||
state.add([nested(8) as never]);
|
||||
assert.equal(state.list().length, 8);
|
||||
state.add([nested(3) as never, { name: "Other category" }]);
|
||||
assert.equal(state.list().length, 4);
|
||||
const before = state.snapshot();
|
||||
assert.throws(() => state.add([nested(9) as never]), /depth must not exceed 8 levels/);
|
||||
assert.throws(() => state.add([nested(4) as never]), /depth must not exceed 3 levels/);
|
||||
assert.deepEqual(state.snapshot(), before);
|
||||
assert.throws(() => state.update([{ id: 4, parentId: 3 }]), /depth must not exceed 3 levels/);
|
||||
assert.deepEqual(state.snapshot(), before);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
state.restore({
|
||||
tasks: [
|
||||
{ id: 1, name: "Category", status: "pending", parentId: null, childrenIds: [2] },
|
||||
{ id: 2, name: "Item", status: "pending", parentId: 1, childrenIds: [3] },
|
||||
{ id: 3, name: "Leaf", status: "pending", parentId: 2, childrenIds: [4] },
|
||||
{ id: 4, name: "Too deep", status: "pending", parentId: 3, childrenIds: [] },
|
||||
],
|
||||
nextId: 5,
|
||||
}),
|
||||
/depth must not exceed 3 levels/,
|
||||
);
|
||||
assert.deepEqual(state.snapshot(), before);
|
||||
});
|
||||
|
||||
|
||||
+88
-6
@@ -92,11 +92,11 @@ test("collapses completed widget subtrees while keeping list trees expanded", ()
|
||||
|
||||
const widgetLines = renderWidgetTree(state.list(), plainTheme as never, 80).map(stripAnsi);
|
||||
assert.deepEqual(widgetLines, [
|
||||
" ○ 1. Release",
|
||||
" ├─ ○ 2. Completed branch · 2/2 done",
|
||||
" └─ ○ 5. Open branch",
|
||||
" └─ ○ 6. Open leaf",
|
||||
" ○ 7. Pending parent · 1/1 done",
|
||||
" I. ○ Release",
|
||||
" ├─ 1. ○ Completed branch · 2/2 done",
|
||||
" └─ 2. ○ Open branch",
|
||||
" └─ ○ Open leaf",
|
||||
" II. ○ Pending parent · 1/1 done",
|
||||
]);
|
||||
|
||||
const fullTree = renderTree(state.list(), plainTheme as never, 80).map(stripAnsi);
|
||||
@@ -109,7 +109,75 @@ test("collapses completed widget subtrees while keeping list trees expanded", ()
|
||||
assert.ok(reopened.some((line) => line.includes("Done two")));
|
||||
});
|
||||
|
||||
test("caps the widget at twelve task rows with an overflow summary", () => {
|
||||
test("uses sibling-local Roman and numeric widget labels while leaving leaf sub-items unlabeled", () => {
|
||||
const state = new TaskState(() => 0);
|
||||
state.add([
|
||||
{ name: "Category one" },
|
||||
{ name: "Category two" },
|
||||
{ name: "Category three" },
|
||||
{
|
||||
name: "Category four",
|
||||
children: [
|
||||
{ name: "First item", children: [{ name: "Leaf sub-item" }] },
|
||||
{ name: "Second item" },
|
||||
],
|
||||
},
|
||||
{ name: "Category five", children: [{ name: "Reset item numbering" }] },
|
||||
]);
|
||||
|
||||
const lines = renderWidgetTree(state.list(), plainTheme as never, 80).map(stripAnsi);
|
||||
assert.deepEqual(lines, [
|
||||
" I. ○ Category one",
|
||||
" II. ○ Category two",
|
||||
" III. ○ Category three",
|
||||
" IV. ○ Category four",
|
||||
" ├─ 1. ○ First item",
|
||||
" │ └─ ○ Leaf sub-item",
|
||||
" └─ 2. ○ Second item",
|
||||
" V. ○ Category five",
|
||||
" └─ 1. ○ Reset item numbering",
|
||||
]);
|
||||
|
||||
const narrow = renderWidgetTree(state.list(), plainTheme as never, 18);
|
||||
assert.ok(narrow.every((line) => visibleWidth(line) <= 18));
|
||||
|
||||
const statusPriority = renderWidgetTree(state.list(), plainTheme as never, 6).map(stripAnsi);
|
||||
assert.match(statusPriority[3] ?? "", /○/);
|
||||
const childStatusPriority = renderWidgetTree(state.list(), plainTheme as never, 8).map(stripAnsi);
|
||||
assert.match(childStatusPriority[4] ?? "", /○/);
|
||||
|
||||
const timedState = new TaskState(() => 0);
|
||||
timedState.add([
|
||||
{ name: "Timed category", status: "in-progress", children: [{ name: "Timed item", status: "in-progress" }] },
|
||||
]);
|
||||
const timedRoot = stripAnsi(renderWidgetTree(timedState.list(), plainTheme as never, 15)[0] ?? "");
|
||||
const timedChild = stripAnsi(renderWidgetTree(timedState.list(), plainTheme as never, 18)[1] ?? "");
|
||||
assert.match(timedRoot, /◉/);
|
||||
assert.match(timedChild, /◉/);
|
||||
});
|
||||
|
||||
test("renumbers widget labels after task reordering and reparenting", () => {
|
||||
const state = new TaskState(() => 0);
|
||||
state.add([
|
||||
{ name: "Category A", children: [{ name: "A1" }, { name: "A2" }] },
|
||||
{ name: "Category B", children: [{ name: "B1" }] },
|
||||
]);
|
||||
state.update([
|
||||
{ id: 4, position: 0 },
|
||||
{ id: 3, parentId: 4, position: 0 },
|
||||
]);
|
||||
|
||||
const lines = renderWidgetTree(state.list(), plainTheme as never, 80).map(stripAnsi);
|
||||
assert.deepEqual(lines, [
|
||||
" I. ○ Category B",
|
||||
" ├─ 1. ○ A2",
|
||||
" └─ 2. ○ B1",
|
||||
" II. ○ Category A",
|
||||
" └─ 1. ○ A1",
|
||||
]);
|
||||
});
|
||||
|
||||
test("caps the widget at twelve task rows with an honest overflow summary", () => {
|
||||
const state = new TaskState(() => 0);
|
||||
state.add(Array.from({ length: 20 }, (_, index) => ({ name: `Task ${index + 1}` })));
|
||||
|
||||
@@ -119,6 +187,20 @@ test("caps the widget at twelve task rows with an overflow summary", () => {
|
||||
assert.match(lines[11] ?? "", /Task 12/);
|
||||
assert.equal(lines[12], " … 8 more tasks");
|
||||
assert.ok(lines.every((line) => visibleWidth(line) <= 60));
|
||||
|
||||
const collapsedOverflow = new TaskState(() => 0);
|
||||
collapsedOverflow.add([
|
||||
...Array.from({ length: 12 }, (_, index) => ({ name: `Visible ${index + 1}` })),
|
||||
{
|
||||
name: "Hidden completed branch",
|
||||
children: [
|
||||
{ name: "Hidden done one", status: "done" as const },
|
||||
{ name: "Hidden done two", status: "done" as const },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const overflowLines = renderWidgetTree(collapsedOverflow.list(), plainTheme as never, 60).map(stripAnsi);
|
||||
assert.equal(overflowLines[12], " … 3 more tasks");
|
||||
});
|
||||
|
||||
test("hides a completed widget summary before squeezing the task name", () => {
|
||||
|
||||
Reference in New Issue
Block a user