import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react' import App from '../App' const tasksResponse = { tasks: [ { id: 't_1', title: 'Build thing', status: 'ready', assignee: 'developer', priority: 5, age: '1m' }, ], groups: { triage: [], todo: [], ready: [{ id: 't_1', title: 'Build thing', status: 'ready', assignee: 'developer', priority: 5 }], running: [], blocked: [], done: [], archived: [], }, } beforeEach(() => { vi.stubGlobal('fetch', vi.fn(async (url: string, init?: RequestInit) => { if (String(url).includes('/api/tasks/t_1/events')) return new Response(JSON.stringify({ events: [{ kind: 'created' }] })) if (String(url).includes('/api/tasks/t_1/runs')) return new Response(JSON.stringify({ runs: [{ id: 1, status: 'completed' }] })) if (String(url).includes('/api/tasks/t_1/log')) return new Response(JSON.stringify({ log: 'tail', missing: false })) if (String(url).endsWith('/api/tasks/t_1')) return new Response(JSON.stringify({ task: { ...tasksResponse.tasks[0], body: 'Full body' } })) if (String(url).includes('/api/tasks') && init?.method === 'POST') return new Response(JSON.stringify({ task: { id: 't_new', title: 'Created', status: 'todo' } })) if (String(url).includes('/api/tasks')) return new Response(JSON.stringify(tasksResponse)) return new Response(JSON.stringify({ status: 'ok' })) })) }) afterEach(() => { cleanup() vi.unstubAllGlobals() }) describe('App', () => { it('renders board columns', async () => { render() expect(await screen.findByText('ready')).toBeInTheDocument() expect(screen.getByText('blocked')).toBeInTheDocument() }) it('renders task cards with title status and assignee', async () => { render() expect(await screen.findByText('Build thing')).toBeInTheDocument() expect(screen.getByText('developer')).toBeInTheDocument() }) it('opens detail drawer and shows body events and log sections', async () => { render() fireEvent.click(await screen.findByText('Build thing')) expect(await screen.findByText('Full body')).toBeInTheDocument() expect(screen.getByText('Events')).toBeInTheDocument() expect(screen.getByText('Log')).toBeInTheDocument() }) it('create form validates title', async () => { render() fireEvent.click(await screen.findByText('Create task')) fireEvent.click(screen.getByText('Create')) expect(await screen.findByText('Title is required')).toBeInTheDocument() }) it('shows API error state', async () => { vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ detail: { message: 'boom' } }), { status: 500 }))) render() expect(await screen.findByText(/boom/)).toBeInTheDocument() }) it('does not crash when Telegram WebApp is absent', async () => { vi.stubGlobal('Telegram', undefined) render() await waitFor(() => expect(screen.getByText('Hermes Kanban')).toBeInTheDocument()) }) })