diff options
Diffstat (limited to 'web/test')
| -rw-r--r-- | web/test/changestats.test.mjs | 125 | ||||
| -rw-r--r-- | web/test/tab-badges.test.mjs | 110 |
2 files changed, 235 insertions, 0 deletions
diff --git a/web/test/changestats.test.mjs b/web/test/changestats.test.mjs new file mode 100644 index 0000000..5363812 --- /dev/null +++ b/web/test/changestats.test.mjs @@ -0,0 +1,125 @@ +// changestats.test.mjs — Unit tests for changestats display functions. +// +// Run with: node --test web/test/changestats.test.mjs + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { formatChangestats, renderChangestatsBadge } from '../app.js'; + +// ── Mock DOM ─────────────────────────────────────────────────────────────────── + +function makeDoc() { + return { + createElement(tag) { + const el = { + tag, + className: '', + textContent: '', + children: [], + appendChild(child) { this.children.push(child); return child; }, + }; + return el; + }, + }; +} + +// ── formatChangestats ────────────────────────────────────────────────────────── + +describe('formatChangestats', () => { + it('formats valid stats as "N files, +A -R"', () => { + const result = formatChangestats({ files_changed: 5, lines_added: 127, lines_removed: 43 }); + assert.equal(result, '5 files, +127 -43'); + }); + + it('returns empty string for null', () => { + const result = formatChangestats(null); + assert.equal(result, ''); + }); + + it('returns empty string for undefined', () => { + const result = formatChangestats(undefined); + assert.equal(result, ''); + }); + + it('formats zero values correctly', () => { + const result = formatChangestats({ files_changed: 0, lines_added: 0, lines_removed: 0 }); + assert.equal(result, '0 files, +0 -0'); + }); + + it('formats single file correctly', () => { + const result = formatChangestats({ files_changed: 1, lines_added: 10, lines_removed: 2 }); + assert.equal(result, '1 files, +10 -2'); + }); +}); + +// ── renderChangestatsBadge ───────────────────────────────────────────────────── + +describe('renderChangestatsBadge', () => { + it('returns element with class changestats-badge for valid stats', () => { + const doc = makeDoc(); + const el = renderChangestatsBadge({ files_changed: 5, lines_added: 127, lines_removed: 43 }, doc); + assert.ok(el, 'element should not be null'); + assert.equal(el.className, 'changestats-badge'); + }); + + it('returns element with correct text content', () => { + const doc = makeDoc(); + const el = renderChangestatsBadge({ files_changed: 5, lines_added: 127, lines_removed: 43 }, doc); + assert.equal(el.textContent, '5 files, +127 -43'); + }); + + it('returns null for null stats', () => { + const doc = makeDoc(); + const el = renderChangestatsBadge(null, doc); + assert.equal(el, null); + }); + + it('returns null for undefined stats', () => { + const doc = makeDoc(); + const el = renderChangestatsBadge(undefined, doc); + assert.equal(el, null); + }); +}); + +// ── State-based visibility ──────────────────────────────────────────────────── +// +// Changestats badge should appear on COMPLETED (and READY) tasks that have +// changestats data, and must not appear on QUEUED tasks. + +const CHANGESTATS_STATES = new Set(['COMPLETED', 'READY']); + +function shouldShowChangestats(task) { + return CHANGESTATS_STATES.has(task.state) && task.changestats != null; +} + +describe('changestats badge visibility by task state', () => { + it('COMPLETED task with changestats shows badge', () => { + const task = { state: 'COMPLETED', changestats: { files_changed: 3, lines_added: 50, lines_removed: 10 } }; + assert.equal(shouldShowChangestats(task), true); + }); + + it('READY task with changestats shows badge', () => { + const task = { state: 'READY', changestats: { files_changed: 1, lines_added: 5, lines_removed: 2 } }; + assert.equal(shouldShowChangestats(task), true); + }); + + it('QUEUED task hides changestats', () => { + const task = { state: 'QUEUED', changestats: { files_changed: 3, lines_added: 50, lines_removed: 10 } }; + assert.equal(shouldShowChangestats(task), false); + }); + + it('COMPLETED task without changestats hides badge', () => { + const task = { state: 'COMPLETED', changestats: null }; + assert.equal(shouldShowChangestats(task), false); + }); + + it('RUNNING task hides changestats', () => { + const task = { state: 'RUNNING', changestats: null }; + assert.equal(shouldShowChangestats(task), false); + }); + + it('PENDING task hides changestats', () => { + const task = { state: 'PENDING', changestats: null }; + assert.equal(shouldShowChangestats(task), false); + }); +}); diff --git a/web/test/tab-badges.test.mjs b/web/test/tab-badges.test.mjs new file mode 100644 index 0000000..c07338f --- /dev/null +++ b/web/test/tab-badges.test.mjs @@ -0,0 +1,110 @@ +// tab-badges.test.mjs — TDD tests for computeTabBadgeCounts +// +// Tests the pure function that computes badge counts for the +// 'interrupted', 'ready', and 'running' tabs. +// +// Run with: node --test web/test/tab-badges.test.mjs + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +// ── Inline implementation (will be replaced by import once exported) ─────────── + +const INTERRUPTED_STATES = new Set(['CANCELLED', 'FAILED', 'BUDGET_EXCEEDED', 'BLOCKED']); + +function computeTabBadgeCounts(tasks) { + let interrupted = 0; + let ready = 0; + let running = 0; + for (const t of tasks) { + if (INTERRUPTED_STATES.has(t.state)) interrupted++; + if (t.state === 'READY') ready++; + if (t.state === 'RUNNING') running++; + } + return { interrupted, ready, running }; +} + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +function makeTask(state) { + return { id: state, name: `task-${state}`, state }; +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +describe('computeTabBadgeCounts', () => { + it('returns all zeros for empty task list', () => { + assert.deepEqual(computeTabBadgeCounts([]), { interrupted: 0, ready: 0, running: 0 }); + }); + + it('counts RUNNING tasks', () => { + const tasks = [makeTask('RUNNING'), makeTask('RUNNING'), makeTask('QUEUED')]; + const counts = computeTabBadgeCounts(tasks); + assert.equal(counts.running, 2); + assert.equal(counts.ready, 0); + assert.equal(counts.interrupted, 0); + }); + + it('counts READY tasks', () => { + const tasks = [makeTask('READY'), makeTask('READY'), makeTask('QUEUED')]; + const counts = computeTabBadgeCounts(tasks); + assert.equal(counts.ready, 2); + assert.equal(counts.running, 0); + assert.equal(counts.interrupted, 0); + }); + + it('counts CANCELLED as interrupted', () => { + const counts = computeTabBadgeCounts([makeTask('CANCELLED')]); + assert.equal(counts.interrupted, 1); + }); + + it('counts FAILED as interrupted', () => { + const counts = computeTabBadgeCounts([makeTask('FAILED')]); + assert.equal(counts.interrupted, 1); + }); + + it('counts BUDGET_EXCEEDED as interrupted', () => { + const counts = computeTabBadgeCounts([makeTask('BUDGET_EXCEEDED')]); + assert.equal(counts.interrupted, 1); + }); + + it('counts BLOCKED as interrupted', () => { + const counts = computeTabBadgeCounts([makeTask('BLOCKED')]); + assert.equal(counts.interrupted, 1); + }); + + it('does not count COMPLETED as interrupted', () => { + const counts = computeTabBadgeCounts([makeTask('COMPLETED')]); + assert.equal(counts.interrupted, 0); + }); + + it('does not count TIMED_OUT as interrupted', () => { + const counts = computeTabBadgeCounts([makeTask('TIMED_OUT')]); + assert.equal(counts.interrupted, 0); + }); + + it('counts across multiple states simultaneously', () => { + const tasks = [ + makeTask('RUNNING'), + makeTask('RUNNING'), + makeTask('READY'), + makeTask('CANCELLED'), + makeTask('FAILED'), + makeTask('BLOCKED'), + makeTask('QUEUED'), + makeTask('COMPLETED'), + ]; + const counts = computeTabBadgeCounts(tasks); + assert.equal(counts.running, 2); + assert.equal(counts.ready, 1); + assert.equal(counts.interrupted, 3); + }); + + it('returns zero for a tab when no tasks match that state', () => { + const tasks = [makeTask('QUEUED'), makeTask('PENDING'), makeTask('COMPLETED')]; + const counts = computeTabBadgeCounts(tasks); + assert.equal(counts.running, 0); + assert.equal(counts.ready, 0); + assert.equal(counts.interrupted, 0); + }); +}); |
