// stories.test.mjs — TDD tests for stories UI functions. // // Run with: node --test web/test/stories.test.mjs import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { renderStoryCard, storyStatusLabel } from '../app.js'; // ── Minimal DOM mock ────────────────────────────────────────────────────────── function makeMockDoc() { function createElement(tag) { return { tag, className: '', textContent: '', innerHTML: '', hidden: false, dataset: {}, children: [], _listeners: {}, style: {}, appendChild(child) { this.children.push(child); return child; }, prepend(...nodes) { this.children.unshift(...nodes); }, append(...nodes) { nodes.forEach(n => this.children.push(n)); }, addEventListener(ev, fn) { this._listeners[ev] = fn; }, querySelector(sel) { const cls = sel.startsWith('.') ? sel.slice(1) : null; function search(el) { if (cls && el.className && el.className.split(' ').includes(cls)) return el; for (const c of el.children || []) { const found = search(c); if (found) return found; } return null; } return search(this); }, querySelectorAll(sel) { const cls = sel.startsWith('.') ? sel.slice(1) : null; const results = []; function search(el) { if (cls && el.className && el.className.split(' ').includes(cls)) results.push(el); for (const c of el.children || []) search(c); } search(this); return results; }, }; } return { createElement }; } function makeStory(overrides = {}) { return { id: 'story-1', name: 'Add login page', project_id: 'claudomator', branch_name: 'story/add-login-page', status: 'PENDING', created_at: '2026-03-25T10:00:00Z', updated_at: '2026-03-25T10:00:00Z', ...overrides, }; } // ── storyStatusLabel ────────────────────────────────────────────────────────── describe('storyStatusLabel', () => { it('returns human-readable label for PENDING', () => { assert.equal(storyStatusLabel('PENDING'), 'Pending'); }); it('returns human-readable label for IN_PROGRESS', () => { assert.equal(storyStatusLabel('IN_PROGRESS'), 'In Progress'); }); it('returns human-readable label for SHIPPABLE', () => { assert.equal(storyStatusLabel('SHIPPABLE'), 'Shippable'); }); it('returns human-readable label for DEPLOYED', () => { assert.equal(storyStatusLabel('DEPLOYED'), 'Deployed'); }); it('returns human-readable label for VALIDATING', () => { assert.equal(storyStatusLabel('VALIDATING'), 'Validating'); }); it('returns human-readable label for REVIEW_READY', () => { assert.equal(storyStatusLabel('REVIEW_READY'), 'Review Ready'); }); it('returns human-readable label for NEEDS_FIX', () => { assert.equal(storyStatusLabel('NEEDS_FIX'), 'Needs Fix'); }); it('falls back to the raw status for unknown values', () => { assert.equal(storyStatusLabel('UNKNOWN_STATE'), 'UNKNOWN_STATE'); }); }); // ── renderStoryCard ─────────────────────────────────────────────────────────── describe('renderStoryCard', () => { let doc; beforeEach(() => { doc = makeMockDoc(); }); it('renders the story name', () => { const card = renderStoryCard(makeStory(), doc); function findText(el, text) { if (el.textContent === text) return true; return (el.children || []).some(c => findText(c, text)); } assert.ok(findText(card, 'Add login page'), 'card should contain story name'); }); it('has story-card class', () => { const card = renderStoryCard(makeStory(), doc); assert.ok(card.className.split(' ').includes('story-card'), 'root element should have story-card class'); }); it('status badge has data-status matching story status', () => { const card = renderStoryCard(makeStory({ status: 'IN_PROGRESS' }), doc); const badge = card.querySelector('.story-status-badge'); assert.ok(badge, 'badge element should exist'); assert.equal(badge.dataset.status, 'IN_PROGRESS'); }); it('status badge shows human-readable label', () => { const card = renderStoryCard(makeStory({ status: 'REVIEW_READY' }), doc); const badge = card.querySelector('.story-status-badge'); assert.equal(badge.textContent, 'Review Ready'); }); it('shows project_id', () => { const card = renderStoryCard(makeStory({ project_id: 'nav' }), doc); function findText(el, text) { if (el.textContent === text) return true; return (el.children || []).some(c => findText(c, text)); } assert.ok(findText(card, 'nav'), 'card should show project_id'); }); it('shows branch_name when present', () => { const card = renderStoryCard(makeStory({ branch_name: 'story/my-feature' }), doc); function findText(el, text) { if (el.textContent && el.textContent.includes(text)) return true; return (el.children || []).some(c => findText(c, text)); } assert.ok(findText(card, 'story/my-feature'), 'card should show branch_name'); }); it('does not show branch section when branch_name is empty', () => { const card = renderStoryCard(makeStory({ branch_name: '' }), doc); const branchEl = card.querySelector('.story-branch'); assert.ok(!branchEl, 'no .story-branch element when branch is empty'); }); it('card dataset.storyId is set to story id', () => { const card = renderStoryCard(makeStory({ id: 'abc-123' }), doc); assert.equal(card.dataset.storyId, 'abc-123'); }); });