2026-08-09 10:37:32 -07:00
|
|
|
import { describe, test, expect, vi, afterEach } from 'vitest';
|
|
|
|
|
import {
|
|
|
|
|
installConsoleErrorBuffer,
|
|
|
|
|
getRecentConsoleErrors,
|
|
|
|
|
__resetConsoleErrorBuffer,
|
|
|
|
|
} from './console-buffer';
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
__resetConsoleErrorBuffer();
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('console error buffer', () => {
|
|
|
|
|
test('captures console.error calls', () => {
|
|
|
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
console.error('boom', 42);
|
|
|
|
|
const entries = getRecentConsoleErrors();
|
|
|
|
|
expect(entries).toHaveLength(1);
|
|
|
|
|
expect(entries[0].message).toBe('boom 42');
|
|
|
|
|
expect(typeof entries[0].ts).toBe('number');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('always chains to the original console.error', () => {
|
|
|
|
|
const original = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
console.error('passed through');
|
|
|
|
|
expect(original).toHaveBeenCalledWith('passed through');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('retains only the last 20 entries, oldest first', () => {
|
|
|
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
for (let i = 0; i < 25; i++) console.error(`e${i}`);
|
|
|
|
|
const entries = getRecentConsoleErrors();
|
|
|
|
|
expect(entries).toHaveLength(20);
|
|
|
|
|
expect(entries[0].message).toBe('e5');
|
|
|
|
|
expect(entries[19].message).toBe('e24');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('truncates a long message to 500 characters', () => {
|
|
|
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
console.error('x'.repeat(900));
|
|
|
|
|
expect(getRecentConsoleErrors()[0].message).toHaveLength(500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('installing twice does not double-capture', () => {
|
|
|
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
console.error('once');
|
|
|
|
|
expect(getRecentConsoleErrors()).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('captures window error events', () => {
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
window.dispatchEvent(new ErrorEvent('error', { message: 'window blew up' }));
|
|
|
|
|
expect(getRecentConsoleErrors().some((e) => e.message.includes('window blew up'))).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('getRecentConsoleErrors returns a copy, not the live array', () => {
|
|
|
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
console.error('a');
|
|
|
|
|
const first = getRecentConsoleErrors();
|
|
|
|
|
first.push({ ts: 0, message: 'injected' });
|
|
|
|
|
expect(getRecentConsoleErrors()).toHaveLength(1);
|
|
|
|
|
});
|
2026-08-09 10:51:08 -07:00
|
|
|
|
|
|
|
|
test('reinstalling after external code wraps console.error does not double-record, and reset restores the true original', () => {
|
|
|
|
|
const trueOriginal = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
|
|
|
|
|
// External code wraps our patch without knowing anything about our
|
|
|
|
|
// marker convention -- this is exactly what a third-party script or
|
|
|
|
|
// another monitor might do.
|
|
|
|
|
const ourPatch = console.error;
|
|
|
|
|
const external = vi.fn((...args: unknown[]) => {
|
|
|
|
|
ourPatch(...args);
|
|
|
|
|
});
|
|
|
|
|
console.error = external;
|
|
|
|
|
|
|
|
|
|
installConsoleErrorBuffer();
|
|
|
|
|
console.error('dup-check');
|
|
|
|
|
|
|
|
|
|
expect(getRecentConsoleErrors().filter((e) => e.message === 'dup-check')).toHaveLength(1);
|
|
|
|
|
|
|
|
|
|
__resetConsoleErrorBuffer();
|
|
|
|
|
expect(console.error).toBe(trueOriginal);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('surviving module re-execution: reinstalling after the module reloads does not re-wrap an already-patched console.error', async () => {
|
|
|
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
|
const mod1 = await import('./console-buffer');
|
|
|
|
|
mod1.installConsoleErrorBuffer();
|
|
|
|
|
const patchedAfterFirstInstall = console.error;
|
|
|
|
|
|
|
|
|
|
// Simulate HMR: the module graph re-evaluates, producing a fresh module
|
|
|
|
|
// instance with its own reset top-level state, while the *global*
|
|
|
|
|
// console.error is still whatever the previous instance patched it to.
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
const mod2 = await import('./console-buffer');
|
|
|
|
|
mod2.installConsoleErrorBuffer();
|
|
|
|
|
|
|
|
|
|
// The marker on the live console.error -- not module-local state -- is
|
|
|
|
|
// what installConsoleErrorBuffer() consults, so the reloaded instance
|
|
|
|
|
// must recognize the existing patch and leave it alone rather than
|
|
|
|
|
// wrapping it a second time.
|
|
|
|
|
expect(console.error).toBe(patchedAfterFirstInstall);
|
|
|
|
|
|
|
|
|
|
console.error('once-across-reload');
|
|
|
|
|
expect(
|
|
|
|
|
mod1.getRecentConsoleErrors().filter((e) => e.message === 'once-across-reload')
|
|
|
|
|
).toHaveLength(1);
|
|
|
|
|
|
|
|
|
|
mod1.__resetConsoleErrorBuffer();
|
|
|
|
|
});
|
2026-08-09 10:37:32 -07:00
|
|
|
});
|