Fixes race condition where WebSocket disconnect event wasn't being processed within React's rendering cycle, causing intermittent CI failures. Wrapping mockOnClose() in act() ensures React state updates are flushed before assertions run. Resolves #237
649 lines
20 KiB
TypeScript
649 lines
20 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { render, screen, waitFor, act } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { LiveLogViewer } from '../LiveLogViewer';
|
|
import * as logsApi from '../../api/logs';
|
|
|
|
// Mock the connectLiveLogs and connectSecurityLogs functions
|
|
vi.mock('../../api/logs', async () => {
|
|
const actual = await vi.importActual('../../api/logs');
|
|
return {
|
|
...actual,
|
|
connectLiveLogs: vi.fn(),
|
|
connectSecurityLogs: vi.fn(),
|
|
};
|
|
});
|
|
|
|
describe('LiveLogViewer', () => {
|
|
let mockCloseConnection: ReturnType<typeof vi.fn>;
|
|
let mockOnMessage: ((log: logsApi.LiveLogEntry) => void) | null;
|
|
let mockOnSecurityMessage: ((log: logsApi.SecurityLogEntry) => void) | null;
|
|
let mockOnClose: (() => void) | null;
|
|
|
|
beforeEach(() => {
|
|
mockCloseConnection = vi.fn();
|
|
mockOnMessage = null;
|
|
mockOnSecurityMessage = null;
|
|
mockOnClose = null;
|
|
|
|
vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, onMessage, onOpen, _onError, onClose) => {
|
|
mockOnMessage = onMessage;
|
|
mockOnClose = onClose ?? null;
|
|
// Simulate connection success
|
|
if (onOpen) {
|
|
setTimeout(() => onOpen(), 0);
|
|
}
|
|
return mockCloseConnection as () => void;
|
|
});
|
|
|
|
vi.mocked(logsApi.connectSecurityLogs).mockImplementation((_filters, onMessage, onOpen, _onError, onClose) => {
|
|
mockOnSecurityMessage = onMessage;
|
|
mockOnClose = onClose ?? null;
|
|
// Simulate connection success
|
|
if (onOpen) {
|
|
setTimeout(() => onOpen(), 0);
|
|
}
|
|
return mockCloseConnection as () => void;
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('renders the component with initial state', async () => {
|
|
render(<LiveLogViewer />);
|
|
|
|
expect(screen.getByText('Live Security Logs')).toBeTruthy();
|
|
// Initially disconnected until WebSocket opens
|
|
expect(screen.getByText('Disconnected')).toBeTruthy();
|
|
|
|
// Wait for onOpen callback to be called
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Connected')).toBeTruthy();
|
|
});
|
|
|
|
expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
|
|
});
|
|
|
|
it('displays incoming log messages', async () => {
|
|
render(<LiveLogViewer />);
|
|
|
|
// Simulate receiving a log
|
|
const logEntry: logsApi.LiveLogEntry = {
|
|
level: 'info',
|
|
timestamp: '2025-12-09T10:30:00Z',
|
|
message: 'Test log message',
|
|
source: 'test',
|
|
};
|
|
|
|
if (mockOnMessage) {
|
|
mockOnMessage(logEntry);
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Test log message')).toBeTruthy();
|
|
expect(screen.getByText('INFO')).toBeTruthy();
|
|
expect(screen.getByText('[test]')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('filters logs by text', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer />);
|
|
|
|
// Add multiple logs
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'First message' });
|
|
mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Second message' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('First message')).toBeTruthy();
|
|
expect(screen.getByText('Second message')).toBeTruthy();
|
|
});
|
|
|
|
// Apply text filter
|
|
const filterInput = screen.getByPlaceholderText('Filter by text...');
|
|
await user.type(filterInput, 'First');
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('First message')).toBeTruthy();
|
|
expect(screen.queryByText('Second message')).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
it('filters logs by level', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer />);
|
|
|
|
// Add multiple logs
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Info message' });
|
|
mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Error message' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Info message')).toBeTruthy();
|
|
expect(screen.getByText('Error message')).toBeTruthy();
|
|
});
|
|
|
|
// Apply level filter
|
|
const levelSelect = screen.getAllByRole('combobox')[0];
|
|
await user.selectOptions(levelSelect, 'error');
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Info message')).toBeFalsy();
|
|
expect(screen.getByText('Error message')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('pauses and resumes log streaming', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer />);
|
|
|
|
// Add initial log
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Before pause' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Before pause')).toBeTruthy();
|
|
});
|
|
|
|
// Click pause button
|
|
const pauseButton = screen.getByTitle('Pause');
|
|
await user.click(pauseButton);
|
|
|
|
// Verify paused state
|
|
await waitFor(() => {
|
|
expect(screen.getByText('⏸ Paused')).toBeTruthy();
|
|
});
|
|
|
|
// Try to add log while paused (should not appear)
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'During pause' });
|
|
}
|
|
|
|
// Log should not appear
|
|
expect(screen.queryByText('During pause')).toBeFalsy();
|
|
|
|
// Resume
|
|
const resumeButton = screen.getByTitle('Resume');
|
|
await user.click(resumeButton);
|
|
|
|
// Add log after resume
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:02Z', message: 'After resume' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('After resume')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('clears all logs', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer />);
|
|
|
|
// Add logs
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Log 1' });
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'Log 2' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Log 1')).toBeTruthy();
|
|
expect(screen.getByText('Log 2')).toBeTruthy();
|
|
});
|
|
|
|
// Click clear button
|
|
const clearButton = screen.getByTitle('Clear logs');
|
|
await user.click(clearButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Log 1')).toBeFalsy();
|
|
expect(screen.queryByText('Log 2')).toBeFalsy();
|
|
expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('limits the number of stored logs', async () => {
|
|
render(<LiveLogViewer maxLogs={2} />);
|
|
|
|
// Add 3 logs (exceeding maxLogs)
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Log 1' });
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:01Z', message: 'Log 2' });
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:02Z', message: 'Log 3' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
// First log should be removed, only last 2 should remain
|
|
expect(screen.queryByText('Log 1')).toBeFalsy();
|
|
expect(screen.getByText('Log 2')).toBeTruthy();
|
|
expect(screen.getByText('Log 3')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('displays log data when available', async () => {
|
|
render(<LiveLogViewer />);
|
|
|
|
const logWithData: logsApi.LiveLogEntry = {
|
|
level: 'error',
|
|
timestamp: '2025-12-09T10:30:00Z',
|
|
message: 'Error occurred',
|
|
data: { error_code: 500, details: 'Internal server error' },
|
|
};
|
|
|
|
if (mockOnMessage) {
|
|
mockOnMessage(logWithData);
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Error occurred')).toBeTruthy();
|
|
// Check that data is rendered as JSON
|
|
expect(screen.getByText(/"error_code"/)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('closes WebSocket connection on unmount', () => {
|
|
const { unmount } = render(<LiveLogViewer />);
|
|
|
|
expect(logsApi.connectLiveLogs).toHaveBeenCalled();
|
|
|
|
unmount();
|
|
|
|
expect(mockCloseConnection).toHaveBeenCalled();
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
const { container } = render(<LiveLogViewer className="custom-class" />);
|
|
|
|
const element = container.querySelector('.custom-class');
|
|
expect(element).toBeTruthy();
|
|
});
|
|
|
|
it('shows correct connection status', async () => {
|
|
let mockOnOpen: (() => void) | undefined;
|
|
let mockOnError: ((error: Event) => void) | undefined;
|
|
|
|
vi.mocked(logsApi.connectLiveLogs).mockImplementation((_filters, _onMessage, onOpen, onError) => {
|
|
mockOnOpen = onOpen;
|
|
mockOnError = onError;
|
|
return mockCloseConnection as () => void;
|
|
});
|
|
|
|
render(<LiveLogViewer />);
|
|
|
|
// Initially disconnected until onOpen is called
|
|
expect(screen.getByText('Disconnected')).toBeTruthy();
|
|
|
|
// Simulate connection opened
|
|
if (mockOnOpen) {
|
|
mockOnOpen();
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Connected')).toBeTruthy();
|
|
});
|
|
|
|
// Simulate connection error
|
|
if (mockOnError) {
|
|
mockOnError(new Event('error'));
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Disconnected')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('shows no-match message when filters exclude all logs', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer />);
|
|
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Visible' });
|
|
mockOnMessage({ level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Hidden' });
|
|
}
|
|
|
|
await waitFor(() => expect(screen.getByText('Visible')).toBeTruthy());
|
|
|
|
await user.type(screen.getByPlaceholderText('Filter by text...'), 'nomatch');
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('No logs match the current filters.')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('marks connection as disconnected when WebSocket closes', async () => {
|
|
render(<LiveLogViewer />);
|
|
|
|
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
|
|
|
act(() => {
|
|
mockOnClose?.();
|
|
});
|
|
|
|
await waitFor(() => expect(screen.getByText('Disconnected')).toBeTruthy());
|
|
});
|
|
|
|
// ============================================================
|
|
// Security Mode Tests
|
|
// ============================================================
|
|
|
|
describe('Security Mode', () => {
|
|
it('renders in security mode when mode="security"', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
expect(screen.getByText('Security Access Logs')).toBeTruthy();
|
|
expect(logsApi.connectSecurityLogs).toHaveBeenCalled();
|
|
});
|
|
|
|
it('displays security log entries with source badges', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
// Wait for connection to establish
|
|
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
|
|
|
const securityLog: logsApi.SecurityLogEntry = {
|
|
timestamp: '2025-12-12T10:30:00Z',
|
|
level: 'info',
|
|
logger: 'http.log.access',
|
|
client_ip: '192.168.1.100',
|
|
method: 'GET',
|
|
uri: '/api/test',
|
|
status: 200,
|
|
duration: 0.05,
|
|
size: 1024,
|
|
user_agent: 'TestAgent/1.0',
|
|
host: 'example.com',
|
|
source: 'normal',
|
|
blocked: false,
|
|
};
|
|
|
|
if (mockOnSecurityMessage) {
|
|
mockOnSecurityMessage(securityLog);
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('NORMAL')).toBeTruthy();
|
|
expect(screen.getByText('192.168.1.100')).toBeTruthy();
|
|
expect(screen.getByText(/GET \/api\/test → 200/)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('displays blocked requests with special styling', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
// Wait for connection to establish
|
|
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
|
|
|
const blockedLog: logsApi.SecurityLogEntry = {
|
|
timestamp: '2025-12-12T10:30:00Z',
|
|
level: 'warn',
|
|
logger: 'http.handlers.waf',
|
|
client_ip: '10.0.0.1',
|
|
method: 'POST',
|
|
uri: '/admin',
|
|
status: 403,
|
|
duration: 0.001,
|
|
size: 0,
|
|
user_agent: 'Attack/1.0',
|
|
host: 'example.com',
|
|
source: 'waf',
|
|
blocked: true,
|
|
block_reason: 'SQL injection detected',
|
|
};
|
|
|
|
// Send message inside act to properly handle state updates
|
|
await act(async () => {
|
|
if (mockOnSecurityMessage) {
|
|
mockOnSecurityMessage(blockedLog);
|
|
}
|
|
});
|
|
|
|
// Use findBy queries (built-in waiting) instead of single waitFor with multiple assertions
|
|
// This avoids race conditions where one failing assertion causes the entire block to retry
|
|
await screen.findByText('10.0.0.1');
|
|
await screen.findByText(/BLOCKED: SQL injection detected/);
|
|
await screen.findByText(/\[SQL injection detected\]/);
|
|
|
|
// For getAllByText, keep in waitFor but separate from other assertions
|
|
await waitFor(() => {
|
|
// Use getAllByText since 'WAF' appears both in dropdown option and source badge
|
|
const wafElements = screen.getAllByText('WAF');
|
|
expect(wafElements.length).toBeGreaterThanOrEqual(2); // Option + badge
|
|
});
|
|
}, 15000); // 15 second timeout as safeguard
|
|
|
|
it('shows source filter dropdown in security mode', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
// Should have source filter options
|
|
expect(screen.getByText('All Sources')).toBeTruthy();
|
|
expect(screen.getByRole('option', { name: 'WAF' })).toBeTruthy();
|
|
expect(screen.getByRole('option', { name: 'CrowdSec' })).toBeTruthy();
|
|
expect(screen.getByRole('option', { name: 'Rate Limit' })).toBeTruthy();
|
|
expect(screen.getByRole('option', { name: 'ACL' })).toBeTruthy();
|
|
});
|
|
|
|
it('filters by source in security mode', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
// Wait for connection
|
|
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
|
|
|
// Add logs from different sources
|
|
if (mockOnSecurityMessage) {
|
|
mockOnSecurityMessage({
|
|
timestamp: '2025-12-12T10:30:00Z',
|
|
level: 'info',
|
|
logger: 'http.log.access',
|
|
client_ip: '192.168.1.1',
|
|
method: 'GET',
|
|
uri: '/normal-request',
|
|
status: 200,
|
|
duration: 0.01,
|
|
size: 100,
|
|
user_agent: 'Test/1.0',
|
|
host: 'example.com',
|
|
source: 'normal',
|
|
blocked: false,
|
|
});
|
|
mockOnSecurityMessage({
|
|
timestamp: '2025-12-12T10:30:01Z',
|
|
level: 'warn',
|
|
logger: 'http.handlers.waf',
|
|
client_ip: '10.0.0.1',
|
|
method: 'POST',
|
|
uri: '/waf-blocked',
|
|
status: 403,
|
|
duration: 0.001,
|
|
size: 0,
|
|
user_agent: 'Attack/1.0',
|
|
host: 'example.com',
|
|
source: 'waf',
|
|
blocked: true,
|
|
block_reason: 'WAF block',
|
|
});
|
|
}
|
|
|
|
// Wait for logs to appear - normal shows URI, blocked shows block message
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/GET \/normal-request/)).toBeTruthy();
|
|
expect(screen.getByText(/BLOCKED: WAF block/)).toBeTruthy();
|
|
});
|
|
|
|
// Filter by WAF using the source dropdown (second combobox after level)
|
|
const sourceSelects = screen.getAllByRole('combobox');
|
|
const sourceFilterSelect = sourceSelects[1]; // Second combobox is source filter
|
|
|
|
await user.selectOptions(sourceFilterSelect, 'waf');
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText(/GET \/normal-request/)).toBeFalsy();
|
|
expect(screen.getByText(/BLOCKED: WAF block/)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('shows blocked only checkbox in security mode', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
expect(screen.getByText('Blocked only')).toBeTruthy();
|
|
expect(screen.getByRole('checkbox')).toBeTruthy();
|
|
});
|
|
|
|
it('toggles blocked only filter', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
const checkbox = screen.getByRole('checkbox');
|
|
await user.click(checkbox);
|
|
|
|
// Verify checkbox is checked
|
|
expect(checkbox).toBeChecked();
|
|
});
|
|
|
|
it('displays duration for security logs', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
// Wait for connection to establish
|
|
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
|
|
|
const securityLog: logsApi.SecurityLogEntry = {
|
|
timestamp: '2025-12-12T10:30:00Z',
|
|
level: 'info',
|
|
logger: 'http.log.access',
|
|
client_ip: '192.168.1.100',
|
|
method: 'GET',
|
|
uri: '/api/test',
|
|
status: 200,
|
|
duration: 0.123,
|
|
size: 1024,
|
|
user_agent: 'TestAgent/1.0',
|
|
host: 'example.com',
|
|
source: 'normal',
|
|
blocked: false,
|
|
};
|
|
|
|
if (mockOnSecurityMessage) {
|
|
mockOnSecurityMessage(securityLog);
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('123.0ms')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('displays status code with appropriate color for security logs', async () => {
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
// Wait for connection to establish
|
|
await waitFor(() => expect(screen.getByText('Connected')).toBeTruthy());
|
|
|
|
if (mockOnSecurityMessage) {
|
|
mockOnSecurityMessage({
|
|
timestamp: '2025-12-12T10:30:00Z',
|
|
level: 'info',
|
|
logger: 'http.log.access',
|
|
client_ip: '192.168.1.100',
|
|
method: 'GET',
|
|
uri: '/ok',
|
|
status: 200,
|
|
duration: 0.01,
|
|
size: 100,
|
|
user_agent: 'Test/1.0',
|
|
host: 'example.com',
|
|
source: 'normal',
|
|
blocked: false,
|
|
});
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('[200]')).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// Mode Toggle Tests
|
|
// ============================================================
|
|
|
|
describe('Mode Toggle', () => {
|
|
it('switches from application to security mode', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer mode="application" />);
|
|
|
|
expect(screen.getByText('Live Security Logs')).toBeTruthy();
|
|
expect(logsApi.connectLiveLogs).toHaveBeenCalled();
|
|
|
|
// Click security mode button
|
|
const securityButton = screen.getByTitle('Security access logs');
|
|
await user.click(securityButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Security Access Logs')).toBeTruthy();
|
|
expect(logsApi.connectSecurityLogs).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('switches from security to application mode', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer mode="security" />);
|
|
|
|
expect(screen.getByText('Security Access Logs')).toBeTruthy();
|
|
|
|
// Click application mode button
|
|
const appButton = screen.getByTitle('Application logs');
|
|
await user.click(appButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Live Security Logs')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('clears logs when switching modes', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer mode="application" />);
|
|
|
|
// Add a log in application mode
|
|
if (mockOnMessage) {
|
|
mockOnMessage({ level: 'info', timestamp: '2025-12-12T10:30:00Z', message: 'App log' });
|
|
}
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('App log')).toBeTruthy();
|
|
});
|
|
|
|
// Switch to security mode
|
|
const securityButton = screen.getByTitle('Security access logs');
|
|
await user.click(securityButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('App log')).toBeFalsy();
|
|
expect(screen.getByText('No logs yet. Waiting for events...')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('resets filters when switching modes', async () => {
|
|
const user = userEvent.setup();
|
|
render(<LiveLogViewer mode="application" />);
|
|
|
|
// Set a filter
|
|
const filterInput = screen.getByPlaceholderText('Filter by text...');
|
|
await user.type(filterInput, 'test');
|
|
|
|
// Switch to security mode
|
|
const securityButton = screen.getByTitle('Security access logs');
|
|
await user.click(securityButton);
|
|
|
|
await waitFor(() => {
|
|
// Filter should be cleared
|
|
expect(screen.getByPlaceholderText('Filter by text...')).toHaveValue('');
|
|
});
|
|
});
|
|
});
|
|
});
|