feat: add BlockedTriageModal component with tests

- Create BlockedTriageModal component at src/components/shared/blocked-triage-modal.tsx
- Implements modal with blocked task triage functionality
- Uses deriveBlockedIds and buildBlockedByTree from kanban lib
- Each row shows blocker chain and has inline archetype picker
- Modal is scrollable and closes via Escape/close button
- Add corresponding tests at tests/components/blocked-triage-modal.test.tsx
- Register test in package.json test script
This commit is contained in:
zenchantlive 2026-03-01 21:12:46 -08:00
parent c71e3742d9
commit 29eefaf7ec
3 changed files with 263 additions and 1 deletions

View file

@ -0,0 +1,73 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'fs/promises';
import path from 'path';
test('BlockedTriageModal - file exists and exports', async () => {
const filePath = path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx');
const fileContent = await fs.readFile(filePath, 'utf-8');
assert.ok(fileContent.includes('export function BlockedTriageModal'), 'Should export BlockedTriageModal function');
assert.ok(fileContent.includes('export interface BlockedTriageModalProps'), 'Should export BlockedTriageModalProps interface');
});
test('BlockedTriageModal - imports Dialog from shadcn', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('from "@/components/ui/dialog"') || fileContent.includes("from '@/components/ui/dialog'"), 'Should import Dialog components from shadcn');
});
test('BlockedTriageModal - imports deriveBlockedIds and buildBlockedByTree from kanban', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('deriveBlockedIds'), 'Should import deriveBlockedIds');
assert.ok(fileContent.includes('buildBlockedByTree'), 'Should import buildBlockedByTree');
});
test('BlockedTriageModal - imports useArchetypePicker hook', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('useArchetypePicker'), 'Should import useArchetypePicker hook');
});
test('BlockedTriageModal - imports BeadIssue type', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('BeadIssue'), 'Should import BeadIssue type');
});
test('BlockedTriageModal - accepts isOpen, onClose, issues, projectRoot props', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('isOpen:'), 'Should have isOpen prop');
assert.ok(fileContent.includes('onClose:'), 'Should have onClose prop');
assert.ok(fileContent.includes('issues:'), 'Should have issues prop');
assert.ok(fileContent.includes('projectRoot:'), 'Should have projectRoot prop');
});
test('BlockedTriageModal - uses useMemo for blockedIds computation', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('useMemo'), 'Should use useMemo for computing blockedIds');
});
test('BlockedTriageModal - computes blocked tasks using status=blocked AND deriveBlockedIds', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes("status === 'blocked'"), 'Should check explicit blocked status');
assert.ok(fileContent.includes('deriveBlockedIds'), 'Should use deriveBlockedIds');
});
test('BlockedTriageModal - uses buildBlockedByTree for blocker chain display', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('buildBlockedByTree'), 'Should use buildBlockedByTree for blocker chains');
});
test('BlockedTriageModal - has local state for showing/hiding archetype picker per row', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('useState'), 'Should use useState');
assert.ok(fileContent.includes('showPicker') || fileContent.includes('pickerOpen') || fileContent.includes('expandedRow'), 'Should have state for row picker visibility');
});
test('BlockedTriageModal - modal content is scrollable', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
assert.ok(fileContent.includes('overflow-y-auto') || fileContent.includes('overflowAuto') || fileContent.includes('scrollable'), 'Should have scrollable content');
});
test('BlockedTriageModal - reuses useArchetypePicker for row assignments', async () => {
const fileContent = await fs.readFile(path.join(process.cwd(), 'src/components/shared/blocked-triage-modal.tsx'), 'utf-8');
const useArchetypePickerCount = (fileContent.match(/useArchetypePicker/g) || []).length;
assert.ok(useArchetypePickerCount >= 2, 'Should use useArchetypePicker hook');
});