diff --git a/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx b/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx
index d65fde28..ab3afced 100644
--- a/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx
+++ b/frontend/src/components/HomeComponents/DevLogs/__tests__/DevLogs.test.tsx
@@ -1,4 +1,11 @@
-import { render, waitFor, screen } from '@testing-library/react';
+import React from 'react';
+import {
+ render,
+ waitFor,
+ screen,
+ fireEvent,
+ act,
+} from '@testing-library/react';
import { DevLogs } from '../DevLogs';
// Mock UI components - DevLogs uses Button and Select components
@@ -9,14 +16,24 @@ jest.mock('../../../ui/button', () => ({
}));
jest.mock('../../../ui/select', () => ({
- Select: ({ children, value }: any) => (
+ Select: ({ children, value, onValueChange }: any) => (
- {children}
+ {React.Children.map(children, (child) =>
+ React.cloneElement(child, { onValueChange })
+ )}
),
- SelectContent: ({ children }: any) => {children}
,
- SelectItem: ({ children, value }: any) => (
- {children}
+ SelectContent: ({ children, onValueChange }: any) => (
+
+ {React.Children.map(children, (child) =>
+ React.cloneElement(child, { onValueChange })
+ )}
+
+ ),
+ SelectItem: ({ children, value, onValueChange }: any) => (
+ onValueChange(value)}>
+ {children}
+
),
SelectTrigger: ({ children }: any) => {children}
,
SelectValue: ({ placeholder }: any) => {placeholder}
,
@@ -71,6 +88,11 @@ const mockLogs = [
message: 'Error occurred',
operation: 'SYNC_ERROR',
},
+ {
+ timestamp: '2024-01-01T12:03:00Z',
+ level: 'DEBUG',
+ message: 'Debug message',
+ },
];
global.fetch = jest.fn(() =>
@@ -80,60 +102,338 @@ global.fetch = jest.fn(() =>
})
) as jest.Mock;
-describe('DevLogs Content Component', () => {
+describe('DevLogs', () => {
+ const renderDevLogs = (isOpen: boolean) =>
+ render( );
+
beforeEach(() => {
jest.clearAllMocks();
});
- it('renders initial state without fetching logs when isOpen is false', () => {
- const { asFragment } = render( );
+ describe('Snapshots – basic states', () => {
+ it('renders closed dialog correctly', () => {
+ const { asFragment } = renderDevLogs(false);
+ expect(asFragment()).toMatchSnapshot('devlogs-closed');
+ });
+
+ it('renders open dialog with logs correctly', async () => {
+ const { asFragment } = renderDevLogs(true);
+
+ await waitFor(() => {
+ expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument();
+ });
+
+ expect(asFragment()).toMatchSnapshot('devlogs-with-logs');
+ });
- // Should render the UI but not fetch logs
- expect(screen.getByText('No logs available')).toBeInTheDocument();
- expect(fetch).not.toHaveBeenCalled();
- expect(asFragment()).toMatchSnapshot('devlogs-initial-state');
+ it('renders loading state correctly', () => {
+ (fetch as jest.Mock).mockImplementationOnce(() => new Promise(() => {}));
+
+ const { asFragment } = renderDevLogs(true);
+
+ expect(asFragment()).toMatchSnapshot('devlogs-loading');
+ });
+
+ it('renders empty logs state correctly', async () => {
+ (fetch as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([]),
+ })
+ );
+
+ const { asFragment } = renderDevLogs(true);
+
+ await waitFor(() => {
+ expect(screen.getByText('No logs available')).toBeInTheDocument();
+ });
+
+ expect(asFragment()).toMatchSnapshot('devlogs-empty');
+ });
});
- it('renders with logs when isOpen is true', async () => {
- const { asFragment } = render( );
+ describe('Fetching logs', () => {
+ it('calls fetch when dialog is opened', async () => {
+ renderDevLogs(true);
+
+ expect(fetch).toHaveBeenCalledTimes(1);
+
+ await waitFor(() => {
+ expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument();
+ });
+ });
+
+ it('displays logs when fetch succeeds', async () => {
+ renderDevLogs(true);
- await waitFor(() => {
- expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument();
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ expect(screen.getByText('Warning message')).toBeInTheDocument();
+ expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
- // Verify logs are displayed
- expect(screen.getByText('Sync operation started')).toBeInTheDocument();
- expect(screen.getByText('Warning message')).toBeInTheDocument();
- expect(screen.getByText('Error occurred')).toBeInTheDocument();
+ it('shows error toast when fetch fails', async () => {
+ (fetch as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: false,
+ })
+ );
+
+ renderDevLogs(true);
+
+ await waitFor(() => {
+ expect(require('react-toastify').toast.error).toHaveBeenCalledWith(
+ 'Failed to fetch logs',
+ expect.any(Object)
+ );
+ });
+ });
+
+ it('calls fetchLogs when Refresh button is clicked', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument()
+ );
+
+ expect(fetch).toHaveBeenCalledTimes(1);
- expect(asFragment()).toMatchSnapshot('devlogs-with-logs');
+ const refreshButton = screen.getByText('Refresh');
+ fireEvent.click(refreshButton);
+
+ expect(fetch).toHaveBeenCalledTimes(2);
+ });
});
- it('renders loading state when fetching logs', () => {
- (fetch as jest.Mock).mockImplementationOnce(
- () => new Promise(() => {}) // Never resolves to keep loading state
- );
+ describe('Loading & empty states', () => {
+ it('shows loading state when logs are being fetched', () => {
+ (fetch as jest.Mock).mockImplementationOnce(() => new Promise(() => {}));
+
+ renderDevLogs(true);
- const { asFragment } = render( );
+ expect(screen.getByText('Loading logs...')).toBeInTheDocument();
+ });
+
+ it('hides loading state after logs are fetched', async () => {
+ renderDevLogs(true);
- expect(screen.getByText('Loading logs...')).toBeInTheDocument();
- expect(asFragment()).toMatchSnapshot('devlogs-loading');
+ await waitFor(() => {
+ expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument();
+ });
+ });
+
+ it('shows empty state when no logs are returned', async () => {
+ (fetch as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([]),
+ })
+ );
+
+ renderDevLogs(true);
+
+ await waitFor(() => {
+ expect(screen.getByText('No logs available')).toBeInTheDocument();
+ });
+ });
+
+ it('handles null response data gracefully', async () => {
+ (fetch as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve(null),
+ })
+ );
+
+ renderDevLogs(true);
+
+ await waitFor(() => {
+ expect(screen.getByText('No logs available')).toBeInTheDocument();
+ });
+ });
});
- it('renders empty logs state correctly', async () => {
- (fetch as jest.Mock).mockImplementationOnce(() =>
- Promise.resolve({
- ok: true,
- json: () => Promise.resolve([]),
- })
- );
+ describe('Filtering', () => {
+ it('shows all logs by default', async () => {
+ renderDevLogs(true);
- const { asFragment } = render( );
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
- await waitFor(() => {
- expect(screen.getByText('No logs available')).toBeInTheDocument();
+ expect(screen.getByText('Warning message')).toBeInTheDocument();
+ expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
- expect(asFragment()).toMatchSnapshot('devlogs-empty');
+ it('filters INFO logs correctly', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ const filterSelect = screen.getByText('Filter by level');
+ fireEvent.click(filterSelect);
+
+ const infoOption = screen.getByText('INFO');
+ fireEvent.click(infoOption);
+
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument();
+ expect(screen.queryByText('Warning message')).not.toBeInTheDocument();
+ expect(screen.queryByText('Error occurred')).not.toBeInTheDocument();
+ });
+
+ it('filters WARN logs correctly', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ fireEvent.click(screen.getByText('Filter by level'));
+ fireEvent.click(screen.getByText('WARN'));
+
+ expect(screen.getByText('Warning message')).toBeInTheDocument();
+ expect(
+ screen.queryByText('Sync operation started')
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText('Error occurred')).not.toBeInTheDocument();
+ });
+
+ it('filters ERROR logs correctly', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ fireEvent.click(screen.getByText('Filter by level'));
+ fireEvent.click(screen.getByText('ERROR'));
+
+ expect(screen.getByText('Error occurred')).toBeInTheDocument();
+ expect(
+ screen.queryByText('Sync operation started')
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText('Warning message')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Copy actions', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ it('copies a single log correctly', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ const copyButtons = screen.getAllByRole('button', { name: 'CopyIcon' });
+
+ const secondCopyButton = copyButtons[1];
+ fireEvent.click(secondCopyButton);
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
+ `[2024-01-01T12:01:00Z] [WARN] Warning message`
+ );
+
+ const firstCopyButton = copyButtons[0];
+
+ fireEvent.click(firstCopyButton);
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
+ `[2024-01-01T12:00:00Z] [INFO] Sync operation started | Operation: SYNC_START | Sync ID: sync-123`
+ );
+
+ expect(screen.getByText('CheckIcon')).toBeInTheDocument();
+
+ act(() => {
+ jest.advanceTimersByTime(2000);
+ });
+
+ expect(screen.queryByText('CheckIcon')).not.toBeInTheDocument();
+ });
+
+ it('copies all logs correctly', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ const copyAllButton = screen.getByText('Copy All');
+ fireEvent.click(copyAllButton);
+
+ const clipboardCall = (navigator.clipboard.writeText as jest.Mock).mock
+ .calls[0][0];
+
+ expect(clipboardCall).toContain('[INFO] Sync operation started');
+ expect(clipboardCall).toContain('[WARN] Warning message');
+ expect(clipboardCall).toContain('[ERROR] Error occurred');
+ expect(clipboardCall).toContain('[DEBUG] Debug message');
+
+ expect(require('react-toastify').toast.success).toHaveBeenCalledTimes(1);
+ });
+
+ it('disables Copy All button when no logs', async () => {
+ (fetch as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([]),
+ })
+ );
+
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('No logs available')).toBeInTheDocument()
+ );
+
+ const copyAllButton = screen.getByText('Copy All');
+ expect(copyAllButton).toBeDisabled();
+ });
+ });
+
+ describe('Helpers & callbacks', () => {
+ it('applies correct color classes based on log level', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(screen.getByText('Sync operation started')).toBeInTheDocument()
+ );
+
+ expect(screen.getByText('[INFO]')).toHaveClass('text-blue-600', {
+ exact: false,
+ });
+
+ expect(screen.getByText('[WARN]')).toHaveClass('text-yellow-600', {
+ exact: false,
+ });
+
+ expect(screen.getByText('[ERROR]')).toHaveClass('text-red-600', {
+ exact: false,
+ });
+
+ expect(screen.getByText('[DEBUG]')).toHaveClass('text-gray-600', {
+ exact: false,
+ });
+ });
+
+ it('formats timestamps using mocked toLocaleString', async () => {
+ renderDevLogs(true);
+
+ await waitFor(() =>
+ expect(
+ screen.getByText('Mon, 01 Jan 2024 12:00:00 GMT')
+ ).toBeInTheDocument()
+ );
+ });
});
});
diff --git a/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap b/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap
index 1da379a7..8166d36a 100644
--- a/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap
+++ b/frontend/src/components/HomeComponents/DevLogs/__tests__/__snapshots__/DevLogs.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`DevLogs Content Component renders empty logs state correctly: devlogs-empty 1`] = `
+exports[`DevLogs Snapshots – basic states renders closed dialog correctly: devlogs-closed 1`] = `
`;
-exports[`DevLogs Content Component renders initial state without fetching logs when isOpen is false: devlogs-initial-state 1`] = `
+exports[`DevLogs Snapshots – basic states renders empty logs state correctly: devlogs-empty 1`] = `
`;
-exports[`DevLogs Content Component renders loading state when fetching logs: devlogs-loading 1`] = `
+exports[`DevLogs Snapshots – basic states renders loading state correctly: devlogs-loading 1`] = `
`;
-exports[`DevLogs Content Component renders with logs when isOpen is true: devlogs-with-logs 1`] = `
+exports[`DevLogs Snapshots – basic states renders open dialog with logs correctly: devlogs-with-logs 1`] = `
+
+
+
+
+
+ Mon, 01 Jan 2024 12:03:00 GMT
+
+
+ [DEBUG]
+
+
+
+ Debug message
+
+
+
+
+ CopyIcon
+
+
+
+