// Copyright (c) 2026 Gordon Bolton. MIT License. /** * @vitest-environment jsdom * Tests for client/src/utils/clientLogCapture.js */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { initClientLogCapture } from '../../../client/src/utils/clientLogCapture.js'; describe('clientLogCapture', () => { let fetchMock; let originalConsoleLog; let originalConsoleWarn; let originalConsoleError; beforeEach(() => { vi.useFakeTimers(); // Preserve original console methods originalConsoleLog = console.log; originalConsoleWarn = console.warn; originalConsoleError = console.error; // Reset console methods to standard ones console.log = vi.fn(); console.warn = vi.fn(); console.error = vi.fn(); // Mock window fetch fetchMock = vi.fn(); global.window.fetch = fetchMock; }); afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); // Restore original console methods console.log = originalConsoleLog; console.warn = originalConsoleWarn; console.error = originalConsoleError; }); it('exits early and does not intercept console if status returns disabled', async () => { fetchMock.mockResolvedValue({ ok: true, json: async () => ({ enabled: false }) }); await initClientLogCapture(); expect(fetchMock).toHaveBeenCalledWith('/api/debug/status'); // Console calls should NOT be queued or overridden (i.e. they should just run vi.fn mocks) console.log('Test message'); expect(console.log).toHaveBeenCalledWith('Test message'); expect(fetchMock).not.toHaveBeenCalledWith('/api/debug/client-logs', expect.any(Object)); }); it('hooks console and flushes logs periodically when status returns enabled', async () => { fetchMock.mockImplementation((url, options) => { if (url === '/api/debug/status') { return Promise.resolve({ ok: true, json: async () => ({ enabled: true }) }); } if (url === '/api/debug/client-logs') { return Promise.resolve({ ok: true, json: async () => ({ success: true }) }); } }); await initClientLogCapture(); expect(fetchMock).toHaveBeenCalledWith('/api/debug/status'); // Trigger console logs console.log('Booting app', { config: 'loaded' }); console.warn('Deprecated api call'); console.error('Failed request', new Error('timeout')); // Move timers forward to trigger flush interval (2000ms) await vi.advanceTimersByTimeAsync(2000); expect(fetchMock).toHaveBeenCalledWith('/api/debug/client-logs', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' } })); const lastCall = fetchMock.mock.calls.find(call => call[0] === '/api/debug/client-logs'); expect(lastCall).toBeDefined(); const loggedEntries = JSON.parse(lastCall[1].body); expect(loggedEntries).toHaveLength(4); // Includes the interceptor boot message + 3 logs expect(loggedEntries[1].level).toBe('info'); expect(loggedEntries[1].message).toContain('Booting app {"config":"loaded"}'); expect(loggedEntries[2].level).toBe('warn'); expect(loggedEntries[2].message).toContain('Deprecated api call'); expect(loggedEntries[3].level).toBe('error'); expect(loggedEntries[3].message).toContain('Failed request'); }); });