/** * Test Pattern: Project List * * Exercises the toggle handler's identity contract (CT-1715): * - toggling flips `done` on the addressed item only * - held-reference survival: a reference stashed in a cell BEFORE a toggle * must still `equals()`-match and still drive a subsequent * equals()-located removal AFTER the toggle. The toggle writes through * the element's cell; replacing the array slot with a fresh object * literal would re-mint the item's entity identity and orphan every held * reference. * * Run: deno task cf test packages/patterns/project-list/main.test.tsx --verbose */ import { action, computed, equals, handler, pattern, Writable, } from "commonfabric"; import ProjectList, { toggleItem } from "./main.tsx"; interface ProjectItem { id: string; title: string; done: boolean; } // Seed the list with fresh literals so each item becomes an entity doc with // its own identity (the same shape the pattern's addItem action produces). const seedItems = handler }>( (_event, { items }) => { items.push({ id: "a", title: "First", done: false }); items.push({ id: "b", title: "Second", done: false }); }, ); // Test plumbing: remove the item the held reference points at, locating it // with equals() — proves a reference held across a toggle still drives // operations (it would silently no-op if the toggle had re-minted the // item's entity identity). const removeHeldItem = handler< void, { items: Writable; held: Writable } >((_event, { items, held }) => { const cur = items.get(); const idx = cur.findIndex((i) => equals(held, i)); if (idx >= 0) { items.set(cur.toSpliced(idx, 1)); } }); export default pattern(() => { const itemsCell = new Writable([]); const list = ProjectList({ items: itemsCell }); // Simulates an external holder (selection cell) that read an item once // and keeps the reference across later mutations. Typed non-null // (placeholder initial value) so the cell can be bound as handler state. const heldItem = new Writable({ id: "", title: "", done: false, }); // ========================================================================== // Actions // ========================================================================== const action_seed = seedItems({ items: itemsCell }); const action_stash_held = action(() => { const item = itemsCell.get()[0]; if (item) heldItem.set(item); }); // The REAL exported toggle handler, bound exactly as the row UI binds it. const action_toggle_first = toggleItem({ index: 0, items: itemsCell }); const action_remove_via_held = removeHeldItem({ items: itemsCell, held: heldItem, }); // ========================================================================== // Assertions // ========================================================================== const assert_seeded = computed(() => { const cur = itemsCell.get(); return cur.length === 2 && cur[0]?.title === "First" && cur[1]?.title === "Second"; }); const assert_held_stashed = computed(() => { const h = heldItem.get(); return h.title === "First" && equals(itemsCell.get()[0], h); }); const assert_first_done = computed(() => itemsCell.get()[0]?.done === true); const assert_second_untouched = computed(() => itemsCell.get()[1]?.done === false ); // KEY: the stale-but-once-valid reference still equals()-matches the item // AFTER the toggle updated it. const assert_held_survives_toggle = computed(() => { const h = heldItem.get(); return equals(itemsCell.get()[0], h); }); // The held reference also READS the update (it would show the stale, // orphaned entity if the toggle had re-minted identity). const assert_held_reads_toggle = computed(() => heldItem.get().done === true); const assert_toggled_back = computed(() => itemsCell.get()[0]?.done === false ); // KEY: the held reference still DRIVES an equals()-located removal. const assert_removed_via_held = computed(() => { const cur = itemsCell.get(); return cur.length === 1 && cur[0]?.title === "Second"; }); // ========================================================================== // Test Sequence // ========================================================================== return { tests: [ { action: action_seed }, { assertion: assert_seeded }, // Held-reference survival: stash → toggle → the old reference still // matches, reads the update, and still drives removal. { action: action_stash_held }, { assertion: assert_held_stashed }, { action: action_toggle_first }, { assertion: assert_first_done }, { assertion: assert_second_untouched }, { assertion: assert_held_survives_toggle }, { assertion: assert_held_reads_toggle }, // Toggling again flips back (still through the element's cell). { action: action_toggle_first }, { assertion: assert_toggled_back }, { action: action_remove_via_held }, { assertion: assert_removed_via_held }, ], list, }; });