diff --git a/.changeset/cap-error-list.md b/.changeset/cap-error-list.md new file mode 100644 index 0000000..10b448b --- /dev/null +++ b/.changeset/cap-error-list.md @@ -0,0 +1,5 @@ +--- +'@solidjs/start-devtools': patch +--- + +The toolbar keeps the 200 newest errors, so an error thrown in a loop no longer grows the list without end. diff --git a/src/dev-toolbar/error-list.test.ts b/src/dev-toolbar/error-list.test.ts new file mode 100644 index 0000000..952bd28 --- /dev/null +++ b/src/dev-toolbar/error-list.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { addError, MAX_ERRORS } from './error-list.js'; + +describe('addError', () => { + it('puts the new error first', () => { + expect(addError(['b', 'a'], 'c')).toEqual(['c', 'b', 'a']); + }); + + it('drops the oldest past the limit', () => { + expect(addError(['c', 'b', 'a'], 'd', 3)).toEqual(['d', 'c', 'b']); + }); + + it('keeps 200 by default', () => { + let list: number[] = []; + for (let i = 0; i < MAX_ERRORS + 5; i++) list = addError(list, i); + expect(list).toHaveLength(MAX_ERRORS); + expect(list[0]).toBe(MAX_ERRORS + 4); + }); +}); diff --git a/src/dev-toolbar/error-list.ts b/src/dev-toolbar/error-list.ts new file mode 100644 index 0000000..e30407e --- /dev/null +++ b/src/dev-toolbar/error-list.ts @@ -0,0 +1,12 @@ +/** Most errors the toolbar keeps. An error thrown in a loop drops the oldest. */ +export const MAX_ERRORS = 200; + +/** The list with `error` first, keeping at most `max` of the newest. */ +export function addError(list: readonly T[], error: T, max = MAX_ERRORS): T[] { + const next = [error]; + for (const item of list) { + if (next.length >= max) break; + next.push(item); + } + return next; +} diff --git a/src/dev-toolbar/index.tsx b/src/dev-toolbar/index.tsx index ebff7b2..1dcceb8 100644 --- a/src/dev-toolbar/index.tsx +++ b/src/dev-toolbar/index.tsx @@ -36,6 +36,7 @@ import { installElementTracking, type InspectedComponent } from './inspector/loo import { InspectorOverlay } from './inspector/overlay.js'; import { addIssues, captureIssues, type IssueEntry } from './issues/tracker.js'; import { isExtensionError } from './extension-errors.js'; +import { addError } from './error-list.js'; import { openInEditor } from './open-in-editor.js'; import { CopyReportButton } from './report/CopyReportButton.js'; import { pageRoute } from './report/markdown.js'; @@ -353,7 +354,7 @@ export function DevToolbar(props: DevToolbarProps) { function pushError(error: unknown) { console.error(error); - setErrors((current) => [error, ...current]); + setErrors((current) => addError(current, error)); show('err'); } diff --git a/tests/e2e/devtools.spec.ts b/tests/e2e/devtools.spec.ts index 7a5fa8f..86bc1c0 100644 --- a/tests/e2e/devtools.spec.ts +++ b/tests/e2e/devtools.spec.ts @@ -209,6 +209,26 @@ test('leaves out errors thrown by browser extensions', async ({ page }) => { ); }); +test('keeps the newest errors when an error repeats in a loop', async ({ page }) => { + await page.goto('/'); + + // An error thrown on every frame of a loop would grow the list without end. + await page.evaluate(() => { + for (let i = 1; i <= 205; i++) { + const error = new Error(`frame ${i}`); + window.dispatchEvent(new ErrorEvent('error', { error, message: error.message })); + } + }); + + const tab = page.getByRole('tab', { name: 'Errors', exact: true }); + await expect(tab).toHaveAttribute('aria-selected', 'true'); + await expect(tab).toContainText('200'); + // The newest is shown first, and the oldest five are gone. + await expect(page.locator('[data-solid-error-viewer-error-info-message]')).toHaveText( + 'frame 205', + ); +}); + test('captures client errors', async ({ page }) => { await page.goto('/'); await page.locator('#throw-client-error').click();