162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
// [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]
|