// task-card-di.test.mjs — Unit tests for the createTaskCard DI doc param, // log-tail placeholder (PENDING/QUEUED), and elapsed timer (RUNNING). // // Run with: node --test web/test/task-card-di.test.mjs import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createTaskCard } from '../app.js'; // ── Mock DOM ─────────────────────────────────────────────────────────────────── function makeEl(tag) { return { tag, className: '', textContent: '', title: '', hidden: false, dataset: {}, children: [], classList: { add() {} }, append(...nodes) { for (const n of nodes) { if (n != null && typeof n === 'object') this.children.push(n); } }, appendChild(child) { if (child != null) this.children.push(child); return child; }, addEventListener() {}, setAttribute() {}, getAttribute() { return null; }, }; } function makeDoc() { return { createElement(tag) { return makeEl(tag); }, }; } // Depth-first search for first element with the given class name. function findByClass(el, cls) { for (const child of el.children || []) { if (typeof child.className === 'string' && child.className.split(' ').includes(cls)) return child; const found = findByClass(child, cls); if (found) return found; } return null; } // ── Tests: DI doc param ──────────────────────────────────────────────────────── describe('createTaskCard doc DI param', () => { it('accepts a custom doc and uses it for createElement', () => { const created = []; const doc = { createElement(tag) { const el = makeEl(tag); created.push(tag); return el; }, }; createTaskCard({ id: 't1', state: 'QUEUED', name: 'Test Task' }, doc); assert.ok(created.length > 0, 'doc.createElement should have been called'); assert.ok(created.includes('div'), 'should create at least one div'); assert.ok(created.includes('span'), 'should create at least one span'); }); it('returns a card element whose tag is div', () => { const card = createTaskCard({ id: 't1', state: 'QUEUED', name: 'Test' }, makeDoc()); assert.equal(card.tag, 'div'); assert.equal(card.className, 'task-card'); assert.equal(card.dataset.taskId, 't1'); }); }); // ── Tests: log-tail placeholder (QUEUED) ────────────────────────────────────── describe('log-tail placeholder for queue column', () => { it('QUEUED task has a task-log-placeholder element', () => { const card = createTaskCard({ id: 't1', state: 'QUEUED', name: 'Test' }, makeDoc()); const placeholder = findByClass(card, 'task-log-placeholder'); assert.ok(placeholder, 'QUEUED card should have a task-log-placeholder element'); }); it('QUEUED task log-tail placeholder text is "Waiting to start\u2026"', () => { const card = createTaskCard({ id: 't1', state: 'QUEUED', name: 'Test' }, makeDoc()); const placeholder = findByClass(card, 'task-log-placeholder'); assert.equal(placeholder.textContent, 'Waiting to start\u2026'); }); it('QUEUED task has a task-log-tail wrapper containing the placeholder', () => { const card = createTaskCard({ id: 't1', state: 'QUEUED', name: 'Test' }, makeDoc()); const logTail = findByClass(card, 'task-log-tail'); assert.ok(logTail, 'QUEUED card should have a task-log-tail wrapper'); const placeholder = findByClass(logTail, 'task-log-placeholder'); assert.ok(placeholder, 'task-log-tail should contain task-log-placeholder'); }); it('RUNNING task does not have a task-log-placeholder', () => { const card = createTaskCard({ id: 't1', state: 'RUNNING', name: 'Test' }, makeDoc()); const placeholder = findByClass(card, 'task-log-placeholder'); assert.equal(placeholder, null, 'RUNNING card should not have a task-log-placeholder'); }); it('COMPLETED task does not have a task-log-placeholder', () => { const card = createTaskCard({ id: 't1', state: 'COMPLETED', name: 'Test' }, makeDoc()); const placeholder = findByClass(card, 'task-log-placeholder'); assert.equal(placeholder, null, 'COMPLETED card should not have a task-log-placeholder'); }); }); // ── Tests: elapsed timer (RUNNING) ──────────────────────────────────────────── describe('elapsed timer for RUNNING tasks', () => { it('RUNNING task has a running-elapsed span', () => { const card = createTaskCard( { id: 't1', state: 'RUNNING', name: 'Test', updated_at: '2024-01-01T00:00:00Z' }, makeDoc(), ); const elapsed = findByClass(card, 'running-elapsed'); assert.ok(elapsed, 'RUNNING card should have a running-elapsed element'); }); it('RUNNING task running-elapsed has data-started-at from task.updated_at', () => { const card = createTaskCard( { id: 't1', state: 'RUNNING', name: 'Test', updated_at: '2024-06-15T12:00:00Z' }, makeDoc(), ); const elapsed = findByClass(card, 'running-elapsed'); assert.equal(elapsed.dataset.startedAt, '2024-06-15T12:00:00Z'); }); it('RUNNING task running-elapsed data-started-at is empty string when updated_at is absent', () => { const card = createTaskCard( { id: 't1', state: 'RUNNING', name: 'Test' }, makeDoc(), ); const elapsed = findByClass(card, 'running-elapsed'); assert.equal(elapsed.dataset.startedAt, ''); }); it('running-elapsed is inside the card header', () => { const card = createTaskCard( { id: 't1', state: 'RUNNING', name: 'Test', updated_at: '2024-01-01T00:00:00Z' }, makeDoc(), ); // The header is the first child of the card (a div.task-card-header). const header = findByClass(card, 'task-card-header'); assert.ok(header, 'card should have a task-card-header'); const elapsed = findByClass(header, 'running-elapsed'); assert.ok(elapsed, 'running-elapsed should be inside the task-card-header'); }); it('QUEUED task does not have a running-elapsed span', () => { const card = createTaskCard({ id: 't1', state: 'QUEUED', name: 'Test' }, makeDoc()); const elapsed = findByClass(card, 'running-elapsed'); assert.equal(elapsed, null, 'QUEUED card should not have a running-elapsed element'); }); it('COMPLETED task does not have a running-elapsed span', () => { const card = createTaskCard({ id: 't1', state: 'COMPLETED', name: 'Test' }, makeDoc()); const elapsed = findByClass(card, 'running-elapsed'); assert.equal(elapsed, null, 'COMPLETED card should not have a running-elapsed element'); }); });