// [DEF:frontend.src.components.__tests__.task_log_viewer:Module] // @TIER: CRITICAL // @SEMANTICS: tests, task-log, viewer, mount, components // @PURPOSE: Unit tests for TaskLogViewer component by mounting it and observing the DOM. // @LAYER: UI (Tests) // @RELATION: VERIFIES -> frontend/src/components/TaskLogViewer.svelte // @INVARIANT: Duplicate logs are never appended. Polling only active for in-progress tasks. import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import TaskLogViewer from '../TaskLogViewer.svelte'; import { getTaskLogs } from '../../services/taskService.js'; vi.mock('../../services/taskService.js', () => ({ getTaskLogs: vi.fn() })); vi.mock('../../lib/i18n', () => ({ t: { subscribe: (fn) => { fn({ tasks: { loading: 'Loading...', logs_title: 'Task Logs' }, common: { retry: 'Retry', close: 'Close' } }); return () => { }; } } })); describe('TaskLogViewer Component', () => { beforeEach(() => { vi.clearAllMocks(); }); it('renders loading state initially', () => { getTaskLogs.mockResolvedValue([]); render(TaskLogViewer, { inline: true, taskId: 'task-123' }); expect(screen.getByText('Loading...')).toBeDefined(); }); it('fetches and displays historical logs', async () => { getTaskLogs.mockResolvedValue([ { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' } ]); render(TaskLogViewer, { inline: true, taskId: 'task-123' }); await waitFor(() => { expect(screen.getByText(/Historical log entry/)).toBeDefined(); }); expect(getTaskLogs).toHaveBeenCalledWith('task-123'); }); it('displays error message on fetch failure', async () => { getTaskLogs.mockRejectedValue(new Error('Network error fetching logs')); render(TaskLogViewer, { inline: true, taskId: 'task-123' }); await waitFor(() => { expect(screen.getByText('Network error fetching logs')).toBeDefined(); expect(screen.getByText('Retry')).toBeDefined(); }); }); it('appends real-time logs passed as props', async () => { getTaskLogs.mockResolvedValue([ { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' } ]); const { rerender } = render(TaskLogViewer, { inline: true, taskId: 'task-123', realTimeLogs: [] }); await waitFor(() => { expect(screen.getByText(/Historical log entry/)).toBeDefined(); }); // Simulate receiving a new real-time log await rerender({ inline: true, taskId: 'task-123', realTimeLogs: [ { timestamp: '2024-01-01T00:00:01', level: 'DEBUG', message: 'Realtime log entry' } ] }); await waitFor(() => { expect(screen.getByText(/Realtime log entry/)).toBeDefined(); }); }); it('deduplicates real-time logs that are already in historical logs', async () => { getTaskLogs.mockResolvedValue([ { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' } ]); const { rerender } = render(TaskLogViewer, { inline: true, taskId: 'task-123', realTimeLogs: [] }); await waitFor(() => { expect(screen.getByText(/Duplicate log entry/)).toBeDefined(); }); // Pass the exact same log as realtime await rerender({ inline: true, taskId: 'task-123', realTimeLogs: [ { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' } ] }); // Wait a bit to ensure no explosive re-renders or double additions await new Promise((r) => setTimeout(r, 50)); // In RTL, if there were duplicates, getAllByText would return > 1 elements. // getByText asserts there is exactly *one* match. expect(() => screen.getByText(/Duplicate log entry/)).not.toThrow(); }); // @TEST_FIXTURE valid_viewer it('fetches and displays historical logs in modal mode under valid_viewer fixture', async () => { getTaskLogs.mockResolvedValue([ { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Modal log entry' } ]); render(TaskLogViewer, { show: true, inline: false, taskId: 'task-123' }); await waitFor(() => { expect(screen.getByText(/Modal log entry/)).toBeDefined(); expect(screen.getByText('Task Logs')).toBeDefined(); }); expect(getTaskLogs).toHaveBeenCalledWith('task-123'); }); // @TEST_EDGE no_task_id it('does not fetch logs if taskId is null', () => { render(TaskLogViewer, { inline: true, taskId: null }); expect(getTaskLogs).not.toHaveBeenCalled(); }); // @UX_FEEDBACK it('passes autoScroll feedback properly down to the panel by rendering without crashing', () => { const { component } = render(TaskLogViewer, { inline: true, taskId: 'task-123' }); expect(component).toBeDefined(); }); }); // [/DEF:frontend.src.components.__tests__.task_log_viewer:Module]