diff options
| author | Claudomator Agent <agent@claudomator.local> | 2026-07-06 05:26:24 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator.local> | 2026-07-06 05:26:24 +0000 |
| commit | d2cbe9aa07afdc0aac33c959803d6a6aaef69b8e (patch) | |
| tree | c7861cc4c1a8472d48dff13be98b7d634090191a /web/test | |
| parent | d9db12a25a42d00e0f64ac87e10ac0b612b841a8 (diff) | |
fix(web): correct elapsed timer class/location, log-tail coverage, and tests (task 4 reviewer findings)
- Move elapsed timer span from card header to after meta block; add
task-elapsed class alongside running-elapsed (Finding 1)
- Replace PENDING/QUEUED-only log-tail div with full if/else: placeholder
div.task-log-tail-placeholder for queue states, div.task-log-tail with
dataset.logTarget for all other states (Finding 2)
- Delete web/test/task-card-di.test.mjs; append correct tests to
web/test/tasks-board.test.mjs covering log-tail and elapsed timer
contracts (Finding 3)
- Thread doc parameter through createEditForm, renderSubtaskRollup, and
renderQuestionFooter so createTaskCard is fully testable without a
browser DOM
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 | ||||
| -rw-r--r-- | web/test/tasks-board.test.mjs | 81 |
2 files changed, 81 insertions, 169 deletions
diff --git a/web/test/task-card-di.test.mjs b/web/test/task-card-di.test.mjs deleted file mode 100644 index e5dff74..0000000 --- a/web/test/task-card-di.test.mjs +++ /dev/null @@ -1,169 +0,0 @@ -// 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'); - }); -}); diff --git a/web/test/tasks-board.test.mjs b/web/test/tasks-board.test.mjs index 560e8c8..db61880 100644 --- a/web/test/tasks-board.test.mjs +++ b/web/test/tasks-board.test.mjs @@ -10,6 +10,7 @@ import { TASK_COLUMNS, columnForTaskState, groupTasksByColumn, + createTaskCard, } from '../app.js'; describe('columnForTaskState', () => { @@ -146,3 +147,83 @@ describe('groupTasksByColumn', () => { } }); }); + +// ── createTaskCard: log-tail placeholder + elapsed timer ──────────────────── +// +// createTaskCard uses the real global `document` by default (existing +// convention throughout app.js — e.g. renderEventTimeline(events, doc = +// document)) but accepts an injectable `doc` for Node-based unit testing +// without jsdom, matching the hand-rolled mock-DOM convention already used +// by web/test/task-panel-summary.test.mjs and web/test/render-dedup.test.mjs. + +function makeMockDoc() { + function makeEl(tag) { + return { + tag, + className: '', + classList: { + _set: new Set(), + add(...cls) { cls.forEach(c => this._set.add(c)); }, + toggle(cls, on) { on ? this._set.add(cls) : this._set.delete(cls); }, + contains(cls) { return this._set.has(cls); }, + }, + textContent: '', + title: '', + hidden: false, + dataset: {}, + children: [], + _listeners: {}, + appendChild(child) { this.children.push(child); return child; }, + append(...nodes) { nodes.forEach(n => this.children.push(n)); }, + prepend(...nodes) { this.children.unshift(...nodes); }, + addEventListener(type, fn) { this._listeners[type] = fn; }, + querySelector(sel) { + const cls = sel.split(',')[0].trim().replace(/^\./, ''); + const search = (el) => { + if (el.className && el.className.split(' ').includes(cls)) return el; + if (el.dataset && sel.includes('[data-started-at]') && el.className.includes('task-elapsed') && 'startedAt' in el.dataset) return el; + for (const c of el.children) { + const found = search(c); + if (found) return found; + } + return null; + }; + return search(this); + }, + }; + } + return { createElement: (tag) => makeEl(tag) }; +} + +describe('createTaskCard log-tail placeholder', () => { + it('shows a "waiting to start" placeholder for PENDING/QUEUED tasks (no execution exists yet)', () => { + const doc = makeMockDoc(); + for (const state of ['PENDING', 'QUEUED']) { + const card = createTaskCard({ id: 't1', name: 'Task', state }, doc); + const placeholder = card.querySelector('.task-log-tail-placeholder'); + const tail = card.querySelector('.task-log-tail'); + assert.ok(placeholder, `expected a placeholder for ${state}`); + assert.equal(tail, null, `expected no .task-log-tail element for ${state}`); + } + }); + + it('includes a .task-log-tail element for every non-Queue state', () => { + const doc = makeMockDoc(); + for (const state of ['RUNNING', 'BLOCKED', 'READY', 'FAILED', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED', 'COMPLETED']) { + const card = createTaskCard({ id: 't1', name: 'Task', state }, doc); + const tail = card.querySelector('.task-log-tail'); + assert.ok(tail, `expected .task-log-tail for ${state}`); + } + }); +}); + +describe('createTaskCard elapsed timer', () => { + it('includes a .task-elapsed[data-started-at] element only for RUNNING tasks', () => { + const doc = makeMockDoc(); + const running = createTaskCard({ id: 't1', name: 'Task', state: 'RUNNING', updated_at: '2026-01-01T00:00:00Z' }, doc); + assert.ok(running.querySelector('.task-elapsed[data-started-at]')); + + const ready = createTaskCard({ id: 't2', name: 'Task', state: 'READY' }, doc); + assert.equal(ready.querySelector('.task-elapsed[data-started-at]'), null); + }); +}); |
