summaryrefslogtreecommitdiff
path: root/web/test
diff options
context:
space:
mode:
Diffstat (limited to 'web/test')
-rw-r--r--web/test/task-card-di.test.mjs169
-rw-r--r--web/test/tasks-board.test.mjs81
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);
+ });
+});