import test from "node:test"; import assert from "node:assert/strict"; import { initializeInteractions } from "../src/scripts/interactions.js"; import { initializeFlowScenes } from "../src/scripts/scenes-flow.js"; import { applyCapture, initializePage } from "../src/scripts/main.js"; function node(queries = {}) { const listeners = new Map(); const classes = new Set(); return { dataset: {}, attributes: {}, queries, animations: [], textContent: "", focused: false, classList: { toggle(key, on) { if (on) classes.add(key); else classes.delete(key); }, contains: (key) => classes.has(key), }, querySelector(selector) { return this.queries[selector] ?? null; }, querySelectorAll(selector) { return this.queries[selector] ?? []; }, getElementById(id) { return this.queries[id] ?? null; }, getAttribute(key) { return this.attributes[key]; }, setAttribute(key, value) { this.attributes[key] = value; }, addEventListener(key, fn) { listeners.set(key, [...(listeners.get(key) ?? []), fn]); }, removeEventListener(key, fn) { listeners.set( key, (listeners.get(key) ?? []).filter((item) => item !== fn), ); }, emit(key, event = {}) { (listeners.get(key) ?? []).forEach((fn) => fn(event)); }, listenerCount() { return [...listeners.values()].reduce( (sum, list) => sum + list.length, 0, ); }, focus() { this.focused = true; }, getAnimations() { return this.animations; }, animate() { const item = { cancelled: false, cancel() { this.cancelled = true; }, finished: Promise.resolve(), }; this.animations.push(item); return item; }, getBoundingClientRect() { return { left: 0, top: 0, width: 200, height: 120 }; }, prepend(card) { this.card = card; }, }; } function environment(document, search = "") { const media = node(); media.matches = false; document.documentElement = { dataset: {} }; return { document, location: { search }, matchMedia: () => media, media }; } function button(attribute, value) { const item = node(); item.setAttribute(attribute, value); return item; } test("filter, context, and navigation retain keyboard state and detach listeners", () => { const filterButtons = ["all", "waiting", "connections"].map((value) => button("data-filter-choice", value), ); const contextButtons = ["source", "work", "action"].map((value) => button("data-context-choice", value), ); const rows = ["waiting", "connections", "waiting"].map((kind) => { const item = node(); item.dataset.taskKind = kind; return item; }); const filter = node({ "[data-task-kind]": rows }); filter.querySelectorAll = (selector) => selector.endsWith(":not(.is-muted)") ? rows.filter((row) => !row.classList.contains("is-muted")) : rows; const summary = node(); const menu = node({ summary }); const toggle = node(); const nav = node(); const context = node(); const label = node(); const count = node(); const document = node({ "[data-filter-choice]": filterButtons, "[data-context-choice]": contextButtons, "filter-demo": filter, "context-demo": context, "filter-current-label": label, "filter-result-count": count, "status-menu": menu, "[data-menu-toggle]": toggle, "site-nav": nav, }); const env = environment(document); const dispose = initializeInteractions(document, env); assert.equal(filter.dataset.filter, "waiting"); filterButtons[0].emit("click", { detail: 0 }); filterButtons[1].emit("click", { detail: 1 }); assert.equal(count.textContent, "2 tasks in focus"); assert.equal(label.textContent, "Waiting on you"); assert.equal(filter.animations.length, 1); assert.equal(summary.focused, true); menu.open = true; filterButtons[1].emit("click", { detail: 1 }); assert.equal(menu.open, false); contextButtons[1].emit("click", { detail: 0 }); assert.equal(context.dataset.context, "work"); assert.equal(context.animations.length, 0); toggle.emit("click"); assert.equal(toggle.getAttribute("aria-expanded"), "true"); document.emit("keydown", { key: "x" }); assert.equal(nav.classList.contains("is-open"), true); document.emit("keydown", { key: "Escape" }); assert.equal(toggle.focused, true); toggle.emit("click"); nav.emit("click", { target: { closest: () => null } }); assert.equal(toggle.getAttribute("aria-expanded"), "true"); nav.emit("click", { target: { closest: () => ({}) } }); assert.equal(toggle.getAttribute("aria-expanded"), "false"); menu.open = true; document.emit("keydown", { key: "Escape" }); assert.equal(menu.open, false); env.media.matches = true; env.media.emit("change"); assert.equal(filter.animations[0].cancelled, true); filterButtons[2].emit("click", { detail: 1 }); assert.equal(filter.animations.length, 1); dispose(); assert.equal(document.listenerCount(), 0); assert.ok( [...filterButtons, ...contextButtons, toggle, nav].every( (item) => item.listenerCount() === 0, ), ); }); test("panorama moves, resets, announces status, and cleans up; progression stays selectable", () => { const controls = ["all", "waiting", "invalid"].map((value) => { const item = node(); item.dataset.panoChoice = value; return item; }); const progressButtons = ["see", "review", "move", "invalid"].map((value) => { const item = node(); item.dataset.progressChoice = value; return item; }); const progress = node({ "[data-progress-choice]": progressButtons }); const move = node(); const card = node(); const done = node(); const waiting = node(); const viewport = node(); viewport.scrollWidth = 800; viewport.clientWidth = 200; viewport.scrollLeft = 0; const column = node(); column.getBoundingClientRect = () => ({ left: 250 }); const fields = Object.fromEntries( [ '[data-pano-count="waiting"]', '[data-pano-count="control"]', '[data-pano-count="done"]', "[data-pano-button-label]", "[data-pano-task-title]", "[data-pano-task-badge]", "[data-pano-status]", "[data-pano-empty]", ].map((key) => [key, node()]), ); const panorama = node({ ...fields, "[data-pano-choice]": controls, "[data-pano-move]": move, "#pano-moving-task": card, '[data-pano-destination="done"]': done, '[data-pano-destination="waiting"]': waiting, ".pano-scroll": viewport, ".pano-col-done": column, ".pano-col-waiting": column, ".pano-col-todo": column, }); panorama.dataset.panoState = "waiting"; const document = node({ "[data-progress-stage]": [progress], "[data-pano-state]": [panorama], }); const env = environment(document); const dispose = initializeFlowScenes(document, env); controls[1].emit("click", { detail: 1 }); assert.equal(panorama.dataset.panoFocus, "waiting"); controls[2].emit("click", { detail: 1 }); assert.equal(panorama.dataset.panoFocus, "waiting"); move.emit("click", { detail: 1 }); assert.equal(done.card, card); assert.equal(panorama.dataset.panoState, "done"); assert.equal(fields['[data-pano-count="done"]'].textContent, "2"); assert.match(fields["[data-pano-status]"].textContent, /moved to Done/); assert.equal(card.animations.length, 1); move.emit("click", { detail: 0 }); assert.equal(waiting.card, card); assert.equal(fields["[data-pano-empty]"].hidden, true); assert.equal(card.animations.length, 1); progressButtons[1].emit("click", { detail: 0 }); assert.equal(progress.dataset.progressStage, "review"); assert.equal(progress.dataset.flowMotion, "false"); progressButtons[3].emit("click", { detail: 1 }); assert.equal(progress.dataset.progressStage, "review"); document.documentElement.dataset.capture = "true"; progressButtons[2].emit("click", { detail: 1 }); assert.equal(progress.dataset.flowMotion, "false"); viewport.scrollWidth = 100; controls[0].emit("click", { detail: 1 }); panorama.queries["#pano-moving-task"] = null; move.emit("click", { detail: 1 }); dispose(); assert.equal(move.listenerCount(), 0); assert.ok( [...controls, ...progressButtons].every( (item) => item.listenerCount() === 0, ), ); }); test("capture is applied before optional scenes initialize and an empty page disposes safely", () => { const hero = node(); hero.dataset.captureSection = "hero"; const agent = node(); agent.dataset.captureSection = "agent"; const document = node({ "[data-capture-section]": [hero, agent] }); const env = environment(document, "?capture=1§ion=agent"); const dispose = initializePage(document, env); assert.equal(document.documentElement.dataset.capture, "true"); assert.equal(hero.hidden, true); assert.equal(agent.hidden, false); dispose(); assert.equal(document.listenerCount(), 0); assert.equal(env.media.listenerCount(), 0); const ordinary = node(); environment(ordinary); applyCapture(ordinary, ""); assert.deepEqual(ordinary.documentElement.dataset, {}); });