eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Views (docs/views.md): the fold's clamps and the pure schema expansion.
2// The vectors in test/event-vectors/views.json pin the everyday folds; this
3// file covers the bounds a vector file would bloat on (caps, eviction, byte
4// limits) and proves the expanded schema drives the shared engine.
5import assert from "assert";
6import { viewsApp, viewSchema } from "../web/static/apps/views.js";
7import { listModel, rowModel, initialState, foldSchemaEvent } from "../web/static/schema-engine.js";
8
9let passes = 0;
10let fails = 0;
11function test(name, fn) {
12 try {
13 fn();
14 console.log(`PASS ${name}`);
15 passes++;
16 } catch (e) {
17 console.error(`FAIL ${name}`);
18 console.error(e.message);
19 fails++;
20 }
21}
22
23const fold = (state, event) => viewsApp.events[event.type](state, event);
24const create = (id, ts, extra = {}) => ({
25 type: "view.create",
26 id,
27 title: "T " + id,
28 items: [{ id: "a", title: "Row" }],
29 name: "Eleven",
30 ts,
31 ...extra,
32});
33
34test("item cap: 51 items fold to 50", () => {
35 const items = Array.from({ length: 51 }, (_, i) => ({ title: "row " + i }));
36 const s = fold({ views: [] }, create("vw-1", 1, { items }));
37 assert.strictEqual(s.views[0].items.length, 50);
38});
39
40test("string clamps: oversized fields are cut, never refused", () => {
41 const s = fold(
42 { views: [] },
43 create("vw-1", 1, {
44 title: "x".repeat(200),
45 items: [{ title: "t".repeat(400), subtitle: "s".repeat(400), note: "n".repeat(600) }],
46 })
47 );
48 const v = s.views[0];
49 assert.strictEqual(v.title.length, 80);
50 assert.strictEqual(v.items[0].title.length, 300);
51 assert.strictEqual(v.items[0].subtitle.length, 300);
52 assert.strictEqual(v.items[0].note.length, 500);
53});
54
55test("byte cap: a view over 32KB folds to a no-op", () => {
56 const items = Array.from({ length: 50 }, (_, i) => ({
57 title: "t".repeat(300),
58 subtitle: "s".repeat(300),
59 note: "n".repeat(500),
60 }));
61 const s = fold({ views: [] }, create("vw-big", 1, { items }));
62 assert.deepStrictEqual(s.views, []);
63});
64
65test("eviction: the 21st view evicts the oldest", () => {
66 let s = { views: [] };
67 for (let i = 1; i <= 21; i++) s = fold(s, create("vw-" + i, i));
68 assert.strictEqual(s.views.length, 20);
69 assert.ok(!s.views.some((v) => v.id === "vw-1"), "oldest evicted");
70 assert.strictEqual(s.views[19].id, "vw-21");
71});
72
73test("items without titles drop; an all-dropped event is a no-op", () => {
74 const s = fold({ views: [] }, create("vw-1", 1, { items: [{ subtitle: "no title" }, { url: "https://x.example" }] }));
75 assert.deepStrictEqual(s.views, []);
76});
77
78test("bad event shapes never throw", () => {
79 for (const e of [
80 { type: "view.create" },
81 { type: "view.create", id: "x!", title: "t", items: [{ title: "r" }] },
82 { type: "view.create", id: "ok", title: "t", items: "nope" },
83 { type: "view.update", id: 7 },
84 { type: "view.remove" },
85 ]) {
86 const s = fold({ views: [] }, e);
87 assert.deepStrictEqual(s.views, []);
88 }
89});
90
91test("update keeps the view's position and original ts when the event has none", () => {
92 let s = { views: [] };
93 s = fold(s, create("vw-1", 100));
94 s = fold(s, create("vw-2", 200));
95 s = fold(s, { type: "view.update", id: "vw-1", title: "Fresh", items: [{ title: "new" }], ts: 0 });
96 assert.strictEqual(s.views[0].id, "vw-1", "position kept");
97 assert.strictEqual(s.views[0].title, "Fresh");
98 assert.strictEqual(s.views[0].ts, 100, "original ts kept when the update carries none");
99});
100
101// ---- the schema expansion drives the real engine ---------------------------
102
103const sample = {
104 id: "vw-1",
105 title: "Top GitHub issues",
106 glyph: "⬢",
107 items: [
108 { id: "412", title: "Crash on login", subtitle: "#412 · amy", note: "urgent", url: "https://github.com/o/r/issues/412", accent: true },
109 { id: "9", title: "Dark mode flicker", subtitle: "", note: "", url: "", accent: false },
110 ],
111 by: "Eleven",
112 ts: 1700000000000,
113};
114
115test("viewSchema: the engine lists every item, in order, in one section", () => {
116 const schema = viewSchema(sample);
117 const model = listModel(schema, { items: sample.items }, Date.now(), { query: "", collapsed: {} });
118 assert.strictEqual(model.sections.length, 1);
119 assert.deepStrictEqual(
120 model.sections[0].rows.map((r) => r.item.id),
121 ["412", "9"]
122 );
123});
124
125test("viewSchema: rowModel resolves title/subtitle/note and the accent flag", () => {
126 const schema = viewSchema(sample);
127 const row = rowModel(schema, sample.items[0], Date.now());
128 assert.strictEqual(row.title, "Crash on login");
129 assert.strictEqual(row.subtitle, "#412 · amy");
130 assert.strictEqual(row.note, "urgent");
131 assert.strictEqual(row.accent, true);
132 const bare = rowModel(schema, sample.items[1], Date.now());
133 assert.strictEqual(bare.subtitle, "", "empty subtitle stays empty");
134});
135
136test("viewSchema: search narrows across title/subtitle/note", () => {
137 const schema = viewSchema(sample);
138 const model = listModel(schema, { items: sample.items }, Date.now(), { query: "flicker", collapsed: {} });
139 assert.deepStrictEqual(
140 model.sections[0].rows.map((r) => r.item.id),
141 ["9"]
142 );
143});
144
145test("viewSchema: declares no events, no composer, no ticker — inert by construction", () => {
146 const schema = viewSchema(sample);
147 assert.deepStrictEqual(schema.events, {});
148 assert.strictEqual(schema.views.main.composer, undefined);
149 assert.strictEqual(schema.ticker, undefined);
150 // And the engine accepts it end-to-end: initial state + a stray event no-op.
151 const s0 = initialState(schema);
152 assert.deepStrictEqual(s0, { items: [] });
153 assert.deepStrictEqual(foldSchemaEvent(schema, s0, { type: "view.create" }), s0);
154});
155
156console.log(`\n${passes} passed, ${fails} failed`);
157if (fails) process.exit(1);
158