// event-timeline.test.mjs — Unit tests for the event timeline component. // // Run with: node --test web/test/event-timeline.test.mjs import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { formatEventText, renderEventTimeline, fetchTaskEvents } from '../app.js'; function makeDoc() { return { createElement(tag) { return { tag, className: '', textContent: '', children: [], appendChild(child) { this.children.push(child); return child; }, }; }, }; } describe('formatEventText', () => { it('formats state_change with from/to', () => { assert.equal( formatEventText({ kind: 'state_change', payload: { from: 'QUEUED', to: 'RUNNING' } }), 'State: QUEUED → RUNNING', ); }); it('formats summary text', () => { assert.equal( formatEventText({ kind: 'summary', payload: { text: 'did the thing' } }), 'Summary: did the thing', ); }); it('formats clarification_request and answer', () => { assert.equal( formatEventText({ kind: 'clarification_request', payload: { text: 'which branch?' } }), 'Question: which branch?', ); assert.equal( formatEventText({ kind: 'clarification_answer', payload: { answer: 'main' } }), 'Answer: main', ); }); it('formats subtask_spawned by name', () => { assert.equal( formatEventText({ kind: 'subtask_spawned', payload: { name: 'write tests', subtask_id: 'x' } }), 'Spawned subtask: write tests', ); }); it('falls back to kind for unknown events', () => { assert.equal(formatEventText({ kind: 'mystery', payload: {} }), 'mystery'); }); it('handles null/empty defensively', () => { assert.equal(formatEventText(null), ''); assert.equal(formatEventText({ kind: 'agent_message', payload: { message: 'hi' } }), 'hi'); }); }); describe('renderEventTimeline', () => { it('renders one item per event with actor and text', () => { const events = [ { kind: 'state_change', actor: 'system', payload: { from: 'PENDING', to: 'QUEUED' } }, { kind: 'summary', actor: 'agent', payload: { text: 'done' } }, ]; const ul = renderEventTimeline(events, makeDoc()); assert.equal(ul.className, 'event-timeline'); assert.equal(ul.children.length, 2); // each item has [actor, text] assert.equal(ul.children[0].children[0].textContent, 'system'); assert.equal(ul.children[0].children[1].textContent, 'State: PENDING → QUEUED'); assert.equal(ul.children[1].children[1].textContent, 'Summary: done'); }); it('renders an empty-state item for no events', () => { const ul = renderEventTimeline([], makeDoc()); assert.equal(ul.children.length, 1); assert.equal(ul.children[0].className, 'event-timeline__empty'); }); it('returns null when doc is null', () => { assert.equal(renderEventTimeline([], null), null); }); }); describe('fetchTaskEvents', () => { it('returns parsed array on success', async () => { const stub = async () => ({ ok: true, json: async () => [{ kind: 'summary', payload: {} }] }); const events = await fetchTaskEvents('t1', stub); assert.equal(events.length, 1); }); it('returns [] on non-ok response', async () => { const stub = async () => ({ ok: false, json: async () => ({}) }); assert.deepEqual(await fetchTaskEvents('t1', stub), []); }); it('returns [] when fetch throws', async () => { const stub = async () => { throw new Error('network'); }; assert.deepEqual(await fetchTaskEvents('t1', stub), []); }); it('returns [] when no fetch implementation is available', async () => { assert.deepEqual(await fetchTaskEvents('t1', null), []); }); });