diff --git a/.changeset/safe-console-dom-pipe.md b/.changeset/safe-console-dom-pipe.md new file mode 100644 index 00000000..f8c2c649 --- /dev/null +++ b/.changeset/safe-console-dom-pipe.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-vite': patch +--- + +Avoid piping raw DOM nodes through the console proxy. diff --git a/packages/devtools-vite/src/virtual-console.test.ts b/packages/devtools-vite/src/virtual-console.test.ts index 26f05b88..31788c27 100644 --- a/packages/devtools-vite/src/virtual-console.test.ts +++ b/packages/devtools-vite/src/virtual-console.test.ts @@ -1,8 +1,51 @@ -import { describe, expect, test } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import { generateConsolePipeCode } from './virtual-console' const TEST_VITE_URL = 'http://localhost:5173' +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() + delete (window as any).__TSD_CONSOLE_PIPE_INITIALIZED__ +}) + +function setupWarnConsolePipe() { + const originalWarn = console.warn + const originalWarnMock = vi.fn() + const fetchMock = vi.fn().mockResolvedValue(undefined) + const eventSourceUrls: Array = [] + + class MockEventSource { + onmessage: ((event: MessageEvent) => void) | null = null + onerror: (() => void) | null = null + + constructor(url: string) { + eventSourceUrls.push(url) + } + } + + console.warn = originalWarnMock + vi.stubGlobal('fetch', fetchMock) + vi.stubGlobal('EventSource', MockEventSource) + + const code = generateConsolePipeCode(['warn'], TEST_VITE_URL) + new Function(code)() + + return { + eventSourceUrls, + fetchMock, + originalWarnMock, + restore: () => { + console.warn = originalWarn + }, + } +} + +function getFirstFetchBody(fetchMock: ReturnType) { + const [, init] = fetchMock.mock.calls[0]! + return JSON.parse(init.body) +} + describe('virtual-console', () => { test('generates inline code with specified levels', () => { const code = generateConsolePipeCode(['log', 'error'], TEST_VITE_URL) @@ -70,4 +113,132 @@ describe('virtual-console', () => { expect(code).toContain(TEST_VITE_URL) expect(code).toContain('/__tsd/console-pipe/server') }) + + test('serializes DOM elements before sending logs', async () => { + vi.useFakeTimers() + + const { eventSourceUrls, fetchMock, originalWarnMock, restore } = + setupWarnConsolePipe() + + try { + const button = document.createElement('button') + button.id = 'save' + button.className = 'primary' + button.type = 'button' + button.setAttribute('data-testid', 'save-button') + + console.warn('gesture warning', button) + + await vi.advanceTimersByTimeAsync(100) + + expect(originalWarnMock).toHaveBeenCalledWith('gesture warning', button) + expect(eventSourceUrls).toEqual(['/__tsd/console-pipe/sse']) + expect(fetchMock).toHaveBeenCalledTimes(1) + + const body = getFirstFetchBody(fetchMock) + + expect(body.entries[0]).toMatchObject({ + level: 'warn', + source: 'client', + args: [ + 'gesture warning', + '