diff --git a/npm_modules/cli/debugger/devtools-panel.css b/npm_modules/cli/debugger/devtools-panel.css index 9d4fee45..749ec4ec 100644 --- a/npm_modules/cli/debugger/devtools-panel.css +++ b/npm_modules/cli/debugger/devtools-panel.css @@ -954,7 +954,7 @@ button { .console-section { min-height: 0; - grid-template-rows: minmax(0, 1fr) 32px; + grid-template-rows: 32px minmax(0, 1fr) 32px; } .console-messages { diff --git a/npm_modules/cli/debugger/devtools-panel.html b/npm_modules/cli/debugger/devtools-panel.html index c0e1c3c1..bb07b7af 100644 --- a/npm_modules/cli/debugger/devtools-panel.html +++ b/npm_modules/cli/debugger/devtools-panel.html @@ -137,6 +137,19 @@ role="tabpanel" aria-labelledby="consoleTab" > +
+ Console output + +
diff --git a/npm_modules/cli/debugger/devtools-panel.js b/npm_modules/cli/debugger/devtools-panel.js index 7fc0ee7a..35d3fca3 100644 --- a/npm_modules/cli/debugger/devtools-panel.js +++ b/npm_modules/cli/debugger/devtools-panel.js @@ -74,6 +74,7 @@ const elements = { nodeSummary: document.getElementById('nodeSummary'), elementsSection: document.getElementById('elementsSection'), splitHandle: document.getElementById('splitHandle'), + clearConsoleButton: document.getElementById('clearConsoleButton'), consoleMessages: document.getElementById('consoleMessages'), consoleForm: document.getElementById('consoleForm'), consoleInput: document.getElementById('consoleInput'), @@ -340,9 +341,7 @@ async function connectToInspectedApplication() { const nextTargetKey = `${payload.target.id}:${payload.target.sessionId}:${inspectedTargetNonce}`; if (previousTargetKey !== null && previousTargetKey !== nextTargetKey) { stopConsoleStream(); - state.consoleEntries = []; - state.consoleEntryKeys.clear(); - elements.consoleMessages.innerHTML = ''; + clearConsole(); preparePerformanceForTargetChange(); } state.target = payload.target; @@ -1410,6 +1409,12 @@ function addConsoleEntry(kind, value, timestamp, source) { elements.consoleMessages.scrollTop = elements.consoleMessages.scrollHeight; } +function clearConsole() { + state.consoleEntries = []; + state.consoleEntryKeys.clear(); + elements.consoleMessages.innerHTML = ''; +} + async function evaluateConsoleExpression(expression) { addConsoleEntry('input', expression); try { @@ -1546,6 +1551,7 @@ function wireEvents() { if (json) await navigator.clipboard.writeText(json); }); elements.splitHandle.addEventListener('pointerdown', startSplitResize); + elements.clearConsoleButton.addEventListener('click', clearConsole); elements.consoleForm.addEventListener('submit', event => { event.preventDefault(); const expression = elements.consoleInput.value.trim(); diff --git a/npm_modules/cli/src/debugger/devtoolsPanel.spec.ts b/npm_modules/cli/src/debugger/devtoolsPanel.spec.ts index 71d2744c..6afd197b 100644 --- a/npm_modules/cli/src/debugger/devtoolsPanel.spec.ts +++ b/npm_modules/cli/src/debugger/devtoolsPanel.spec.ts @@ -61,6 +61,7 @@ interface StubElement { scrollHeight: number; scrollTop: number; textContent: string; + value: string; addEventListener(type: string, listener: (event: unknown) => void): void; dispatch(type: string): void; } @@ -74,12 +75,22 @@ interface MockConsoleEventSource { } interface DevToolsConsolePanel { + clearButton: StubElement; + consoleInput: StubElement; consoleMessages: StubElement; liveToggle: StubElement; state: { autoRefresh: boolean; consoleEntries: Array<{ kind: string; value: string }>; + consoleHistory: string[]; + consoleHistoryIndex: number; consoleStream: MockConsoleEventSource | null; + consoleStreamTargetKey: string | null; + performance: { + lastTrace: Record | null; + ownerIdentity: Record | null; + traceActive: boolean; + }; target: { id: string; sessionId: string } | null; }; evaluateConsoleExpression(expression: string): Promise; @@ -424,6 +435,7 @@ describe('integrated DevTools console panel', () => { scrollHeight: 100, scrollTop: 0, textContent: '', + value: '', addEventListener(type: string, listener: (event: unknown) => void): void { listeners.set(type, listener); }, @@ -473,7 +485,7 @@ describe('integrated DevTools console panel', () => { } panel = new Script( - `${source}\n({ consoleMessages: elements.consoleMessages, dispatchWindowEvent: type => windowListeners.get(type)?.(), evaluateConsoleExpression, liveToggle: elements.autoRefreshToggle, startConsoleStream, state, stopConsoleStream })`, + `${source}\n({ clearButton: elements.clearConsoleButton, consoleInput: elements.consoleInput, consoleMessages: elements.consoleMessages, dispatchWindowEvent: type => windowListeners.get(type)?.(), evaluateConsoleExpression, liveToggle: elements.autoRefreshToggle, startConsoleStream, state, stopConsoleStream })`, ).runInNewContext({ EventSource: MockEventSource, URL, @@ -538,6 +550,66 @@ describe('integrated DevTools console panel', () => { expect(panel.consoleMessages.innerHTML).not.toContain('Wrong target'); }); + it('exposes an accessible local clear action that is distinct from Performance controls', () => { + const markup = fs.readFileSync(path.resolve(process.cwd(), 'debugger', 'devtools-panel.html'), 'utf8'); + const clearButton = markup.match(/]*id="clearConsoleButton")[\S\s]*?<\/button>/)?.[0]; + + expect(clearButton).toBeDefined(); + expect(clearButton).toContain('type="button"'); + expect(clearButton).toContain('aria-label="Clear console"'); + expect(clearButton).toContain('aria-controls="consoleMessages"'); + expect(clearButton).not.toContain('data-performance-action'); + }); + + it('clears only local console output while preserving the stream, target, prompt, history, and Performance', () => { + panel.startConsoleStream(); + const stream = eventSources[0]; + if (!stream) throw new Error('Expected the Chromium console stream to connect.'); + const target = panel.state.target; + const streamTargetKey = panel.state.consoleStreamTargetKey; + const performanceOwner = { + inspectedUrl: 'http://127.0.0.1:54321/index.html?valdiDevTools=1', + sessionId: 'web-preview', + targetNonce: 'panel-target-nonce-123456', + }; + const lastTrace = { traceCount: 4 }; + panel.state.consoleHistory.push('first()', 'second()'); + panel.state.consoleHistoryIndex = 1; + panel.consoleInput.value = 'draft expression'; + panel.state.performance.ownerIdentity = performanceOwner; + panel.state.performance.lastTrace = lastTrace; + panel.state.performance.traceActive = true; + const entry = { + level: 'info', + message: 'Renderer output', + sessionId: 'web-preview', + source: 'console', + targetId: 'owl:web-preview', + timestamp: 42, + }; + stream.emit('console', entry); + + panel.clearButton.dispatch('click'); + + expect(panel.state.consoleEntries).toEqual([]); + expect(panel.consoleMessages.innerHTML).toBe(''); + expect(panel.state.consoleStream).toBe(stream); + expect(stream.closed).toBeFalse(); + expect(panel.state.target).toBe(target); + expect(panel.state.consoleStreamTargetKey).toBe(streamTargetKey); + expect(panel.consoleInput.value).toBe('draft expression'); + expect(panel.state.consoleHistory).toEqual(['first()', 'second()']); + expect(panel.state.consoleHistoryIndex).toBe(1); + expect(panel.state.performance.ownerIdentity).toBe(performanceOwner); + expect(panel.state.performance.lastTrace).toBe(lastTrace); + expect(panel.state.performance.traceActive).toBeTrue(); + + stream.emit('console', entry); + + expect(panel.state.consoleEntries).toEqual([jasmine.objectContaining({ kind: 'info', value: 'Renderer output' })]); + expect(panel.consoleMessages.innerHTML).toContain('Renderer output'); + }); + it('isolates reconnects, honors the Live toggle, and tears down on pagehide', () => { panel.startConsoleStream(); const first = eventSources[0];