diff options
| author | Claudomator Agent <agent@claudomator.local> | 2026-07-06 05:12:47 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator.local> | 2026-07-06 05:12:47 +0000 |
| commit | d9db12a25a42d00e0f64ac87e10ac0b612b841a8 (patch) | |
| tree | 8bd36b95d0f9d7252c8d225b668903d023e501e7 /web/test | |
| parent | 18170fd5fc68cee039e2a8b471d3a7d502866454 (diff) | |
feat(web): extend createTaskCard with DI doc param, log-tail placeholder, elapsed timer (task 4)
- Export createTaskCard and add `doc = document` parameter; replace every
document.createElement call inside the function with doc.createElement,
enabling unit testing without a real DOM.
- RUNNING tasks: append a span.running-elapsed (data-started-at, elapsed
text) to the card header, consistent with renderRunningView so
updateRunningElapsed() picks it up automatically.
- PENDING/QUEUED tasks: append div.task-log-tail > p.task-log-placeholder
("Waiting to start…") — the queue column has no execution yet, so a
placeholder is shown instead of an empty log box.
- New web/test/task-card-di.test.mjs covers DI doc plumbing, placeholder
presence/absence, and elapsed-timer presence/absence across states.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/test')
| -rw-r--r-- | web/test/task-card-di.test.mjs | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/web/test/task-card-di.test.mjs b/web/test/task-card-di.test.mjs new file mode 100644 index 0000000..e5dff74 --- /dev/null +++ b/web/test/task-card-di.test.mjs @@ -0,0 +1,169 @@ +// 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'); + }); +}); |
