// Views (docs/views.md): the fold's clamps and the pure schema expansion. // The vectors in test/event-vectors/views.json pin the everyday folds; this // file covers the bounds a vector file would bloat on (caps, eviction, byte // limits) and proves the expanded schema drives the shared engine. import assert from "assert"; import { viewsApp, viewSchema } from "../web/static/apps/views.js"; import { listModel, rowModel, initialState, foldSchemaEvent } from "../web/static/schema-engine.js"; let passes = 0; let fails = 0; function test(name, fn) { try { fn(); console.log(`PASS ${name}`); passes++; } catch (e) { console.error(`FAIL ${name}`); console.error(e.message); fails++; } } const fold = (state, event) => viewsApp.events[event.type](state, event); const create = (id, ts, extra = {}) => ({ type: "view.create", id, title: "T " + id, items: [{ id: "a", title: "Row" }], name: "Eleven", ts, ...extra, }); test("item cap: 51 items fold to 50", () => { const items = Array.from({ length: 51 }, (_, i) => ({ title: "row " + i })); const s = fold({ views: [] }, create("vw-1", 1, { items })); assert.strictEqual(s.views[0].items.length, 50); }); test("string clamps: oversized fields are cut, never refused", () => { const s = fold( { views: [] }, create("vw-1", 1, { title: "x".repeat(200), items: [{ title: "t".repeat(400), subtitle: "s".repeat(400), note: "n".repeat(600) }], }) ); const v = s.views[0]; assert.strictEqual(v.title.length, 80); assert.strictEqual(v.items[0].title.length, 300); assert.strictEqual(v.items[0].subtitle.length, 300); assert.strictEqual(v.items[0].note.length, 500); }); test("byte cap: a view over 32KB folds to a no-op", () => { const items = Array.from({ length: 50 }, (_, i) => ({ title: "t".repeat(300), subtitle: "s".repeat(300), note: "n".repeat(500), })); const s = fold({ views: [] }, create("vw-big", 1, { items })); assert.deepStrictEqual(s.views, []); }); test("eviction: the 21st view evicts the oldest", () => { let s = { views: [] }; for (let i = 1; i <= 21; i++) s = fold(s, create("vw-" + i, i)); assert.strictEqual(s.views.length, 20); assert.ok(!s.views.some((v) => v.id === "vw-1"), "oldest evicted"); assert.strictEqual(s.views[19].id, "vw-21"); }); test("items without titles drop; an all-dropped event is a no-op", () => { const s = fold({ views: [] }, create("vw-1", 1, { items: [{ subtitle: "no title" }, { url: "https://x.example" }] })); assert.deepStrictEqual(s.views, []); }); test("bad event shapes never throw", () => { for (const e of [ { type: "view.create" }, { type: "view.create", id: "x!", title: "t", items: [{ title: "r" }] }, { type: "view.create", id: "ok", title: "t", items: "nope" }, { type: "view.update", id: 7 }, { type: "view.remove" }, ]) { const s = fold({ views: [] }, e); assert.deepStrictEqual(s.views, []); } }); test("update keeps the view's position and original ts when the event has none", () => { let s = { views: [] }; s = fold(s, create("vw-1", 100)); s = fold(s, create("vw-2", 200)); s = fold(s, { type: "view.update", id: "vw-1", title: "Fresh", items: [{ title: "new" }], ts: 0 }); assert.strictEqual(s.views[0].id, "vw-1", "position kept"); assert.strictEqual(s.views[0].title, "Fresh"); assert.strictEqual(s.views[0].ts, 100, "original ts kept when the update carries none"); }); // ---- the schema expansion drives the real engine --------------------------- const sample = { id: "vw-1", title: "Top GitHub issues", glyph: "⬢", items: [ { id: "412", title: "Crash on login", subtitle: "#412 · amy", note: "urgent", url: "https://github.com/o/r/issues/412", accent: true }, { id: "9", title: "Dark mode flicker", subtitle: "", note: "", url: "", accent: false }, ], by: "Eleven", ts: 1700000000000, }; test("viewSchema: the engine lists every item, in order, in one section", () => { const schema = viewSchema(sample); const model = listModel(schema, { items: sample.items }, Date.now(), { query: "", collapsed: {} }); assert.strictEqual(model.sections.length, 1); assert.deepStrictEqual( model.sections[0].rows.map((r) => r.item.id), ["412", "9"] ); }); test("viewSchema: rowModel resolves title/subtitle/note and the accent flag", () => { const schema = viewSchema(sample); const row = rowModel(schema, sample.items[0], Date.now()); assert.strictEqual(row.title, "Crash on login"); assert.strictEqual(row.subtitle, "#412 · amy"); assert.strictEqual(row.note, "urgent"); assert.strictEqual(row.accent, true); const bare = rowModel(schema, sample.items[1], Date.now()); assert.strictEqual(bare.subtitle, "", "empty subtitle stays empty"); }); test("viewSchema: search narrows across title/subtitle/note", () => { const schema = viewSchema(sample); const model = listModel(schema, { items: sample.items }, Date.now(), { query: "flicker", collapsed: {} }); assert.deepStrictEqual( model.sections[0].rows.map((r) => r.item.id), ["9"] ); }); test("viewSchema: declares no events, no composer, no ticker — inert by construction", () => { const schema = viewSchema(sample); assert.deepStrictEqual(schema.events, {}); assert.strictEqual(schema.views.main.composer, undefined); assert.strictEqual(schema.ticker, undefined); // And the engine accepts it end-to-end: initial state + a stray event no-op. const s0 = initialState(schema); assert.deepStrictEqual(s0, { items: [] }); assert.deepStrictEqual(foldSchemaEvent(schema, s0, { type: "view.create" }), s0); }); console.log(`\n${passes} passed, ${fails} failed`); if (fails) process.exit(1);