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('');
|
|
});
|
|
});
|
|
});
|
|
});
|