summaryrefslogtreecommitdiff
path: root/web/test/stories.test.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'web/test/stories.test.mjs')
-rw-r--r--web/test/stories.test.mjs164
1 files changed, 164 insertions, 0 deletions
diff --git a/web/test/stories.test.mjs b/web/test/stories.test.mjs
new file mode 100644
index 0000000..263f202
--- /dev/null
+++ b/web/test/stories.test.mjs
@@ -0,0 +1,164 @@
+// 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');
+ });
+});