UNPKG

chrome-devtools-frontend

Version:
434 lines (389 loc) • 19.8 kB
// Copyright (c) 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import { createConsoleViewMessageWithStubDeps, createStackTrace, } from '../../testing/ConsoleHelpers.js'; import {renderElementIntoDOM} from '../../testing/DOMHelpers.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Console from './console.js'; // The css files aren't exported by the bundle, so we need to import it directly. // eslint-disable-next-line rulesdir/es-modules-import import consoleViewStyles from './consoleView.css.js'; const {urlString} = Platform.DevToolsPath; describe('ConsoleViewMessage', () => { describe('concatErrorDescriptionAndIssueSummary', () => { const {concatErrorDescriptionAndIssueSummary} = Console.ConsoleViewMessage; it('correctly appends the issue summary in case of single line error descriptions', () => { assert.strictEqual( concatErrorDescriptionAndIssueSummary( 'TypeError: Failed to fetch', 'Access blocked by CORS policy: Cross origin requests are not allowed by request mode.'), 'TypeError: Failed to fetch. Access blocked by CORS policy: Cross origin requests are not allowed by request mode.', ); }); it('correctly inserts the issue summary in case of multi-line error descriptions', () => { assert.strictEqual( concatErrorDescriptionAndIssueSummary( 'TypeError: Failed to fetch\n at (index):25:5', 'Access blocked by CORS policy: Cross origin requests are not allowed by request mode.'), 'TypeError: Failed to fetch. Access blocked by CORS policy: Cross origin requests are not allowed by request mode.\n at (index):25:5', ); }); }); }); describeWithMockConnection('ConsoleViewMessage', () => { describe('anchor rendering', () => { it('links to the top frame for normal console message', () => { const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const stackTrace = createStackTrace([ 'USER_ID::userNestedFunction::http://example.com/script.js::40::15', 'USER_ID::userFunction::http://example.com/script.js::10::2', 'APP_ID::entry::http://example.com/app.js::25::10', ]); const messageDetails = { type: Protocol.Runtime.ConsoleAPICalledEventType.Log, stackTrace, }; const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, 'got here', messageDetails); const {message, linkifier} = createConsoleViewMessageWithStubDeps(rawMessage); message.toMessageElement(); // Trigger rendering. sinon.assert.calledOnceWithExactly(linkifier.linkifyStackTraceTopFrame, target, stackTrace); }); it('links to the frame with the logpoint/breakpoint if the stack trace contains the "marker sourceURL"', () => { const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const stackTrace = createStackTrace([ `LOG_ID::eval::${SDK.DebuggerModel.LOGPOINT_SOURCE_URL}::0::15`, 'USER_ID::userFunction::http://example.com/script.js::10::2', 'APP_ID::entry::http://example.com/app.js::25::10', ]); const messageDetails = { type: Protocol.Runtime.ConsoleAPICalledEventType.Log, stackTrace, }; const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, 'value of x is 42', messageDetails); const {message, linkifier} = createConsoleViewMessageWithStubDeps(rawMessage); message.toMessageElement(); // Trigger rendering. const expectedCallFrame = stackTrace.callFrames[1]; // userFunction. sinon.assert.calledOnceWithExactly( linkifier.maybeLinkifyConsoleCallFrame, target, expectedCallFrame, {inlineFrameIndex: 0, revealBreakpoint: true, userMetric: undefined}); }); it('uses the last "marker sourceURL" frame when searching for the breakpoint/logpoint frame', () => { const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const stackTrace = createStackTrace([ `LOG_ID::leakedClosure::${SDK.DebuggerModel.LOGPOINT_SOURCE_URL}::2::3`, 'USER_ID::callback::http://example.com/script.js::5::42', `LOG_ID::eval::${SDK.DebuggerModel.LOGPOINT_SOURCE_URL}::0::15`, 'USER_ID::userFunction::http://example.com/script.js::10::2', 'APP_ID::entry::http://example.com/app.js::25::10', ]); const messageDetails = { type: Protocol.Runtime.ConsoleAPICalledEventType.Log, stackTrace, }; const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, 'value of x is 42', messageDetails); const {message, linkifier} = createConsoleViewMessageWithStubDeps(rawMessage); message.toMessageElement(); // Trigger rendering. const expectedCallFrame = stackTrace.callFrames[3]; // userFunction. sinon.assert.calledOnceWithExactly( linkifier.maybeLinkifyConsoleCallFrame, target, expectedCallFrame, {inlineFrameIndex: 0, revealBreakpoint: true, userMetric: undefined}); }); }); describe('console insights', () => { it('shows a hover button', () => { sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'hasAction') .withArgs('explain.console-message.hover') .returns(true); const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, Protocol.Log.LogEntryLevel.Error, 'got here'); const {message} = createConsoleViewMessageWithStubDeps(rawMessage); const messageElement = message.toMessageElement(); // Trigger rendering. const button = messageElement.querySelector('[aria-label=\'Understand this error. Powered by AI.\']'); assert.strictEqual(button?.textContent, 'Understand this error'); }); it('does not show a hover button if the console message text is empty', () => { sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'hasAction') .withArgs('explain.console-message.hover') .returns(true); const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, Protocol.Log.LogEntryLevel.Error, ''); const {message} = createConsoleViewMessageWithStubDeps(rawMessage); const messageElement = message.toMessageElement(); // Trigger rendering. const button = messageElement.querySelector('[aria-label=\'Understand this error. Powered by AI.\']'); assert.isNull(button); }); it('does not show a hover button for the self-XSS warning message', () => { sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'hasAction') .withArgs('explain.console-message.hover') .returns(true); const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.SELF_XSS, Protocol.Log.LogEntryLevel.Warning, 'Don’t paste code...'); const {message} = createConsoleViewMessageWithStubDeps(rawMessage); const messageElement = message.toMessageElement(); // Trigger rendering. const button = messageElement.querySelector('[aria-label=\'Understand this warning. Powered by AI.\']'); assert.isNull(button); }); }); describe('with ignore listing', () => { const IGNORE_LIST_LINK = 'ignore-list-link'; function findStackPreviewContainer(element: HTMLElement) { const outer = element.querySelector('span.stack-preview-container'); assert.isNotNull(outer); const inner = outer.shadowRoot; assert.isNotNull(inner); return inner; } function findLinks(element: HTMLElement) { const root = findStackPreviewContainer(element); const showAll = root.querySelector('.show-all-link'); assert.isNotNull(showAll); const showLess = root.querySelector('.show-less-link'); assert.isNotNull(showLess); return {showAll, showLess}; } function assertNoLinks(element: HTMLElement) { const {showAll, showLess} = findLinks(element); assert.isFalse(showAll.checkVisibility()); assert.isFalse(showLess.checkVisibility()); } function assertShowAllLink(element: HTMLElement) { const {showAll, showLess} = findLinks(element); assert.isTrue(showAll.checkVisibility()); assert.isFalse(showLess.checkVisibility()); } function assertShowLessLink(element: HTMLElement) { const {showAll, showLess} = findLinks(element); assert.isFalse(showAll.checkVisibility()); assert.isTrue(showLess.checkVisibility()); } function errorMessageForStack(stack: Protocol.Runtime.StackTrace, withBuiltinFrames?: boolean) { const lines = [ 'Error:', ...(stack.callFrames.flatMap(frame => { const line = ` at ${frame.functionName} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`; if (withBuiltinFrames) { return [line, ' at JSON.parse (<anonymous>)']; } return [line]; })), ]; return lines.join('\n'); } function getCallFrames(element: HTMLElement): string[] { const results = []; for (const line of element.querySelectorAll('.formatted-stack-frame,.formatted-builtin-stack-frame')) { if (line.checkVisibility()) { results.push(line.textContent ?? 'Error: line was null or undefined'); } } return results; } function getStructuredCallFrames(element: HTMLElement): string[] { const results = []; for (const line of findStackPreviewContainer(element).querySelectorAll('tbody tr')) { if (line.checkVisibility()) { results.push(line.textContent ?? 'Error: line was null or undefined'); } } return results; } function expandStructuredTrace(element: HTMLElement) { (element.querySelector('.console-message-stack-trace-wrapper > div') as HTMLElement).click(); } function expandIgnored(element: HTMLElement) { const {showAll} = findLinks(element); (showAll.querySelector('.link') as HTMLElement).click(); } function collapseIgnored(element: HTMLElement) { const {showLess} = findLinks(element); (showLess.querySelector('.link') as HTMLElement).click(); } async function createConsoleMessageWithIgnoreListing( ignoreListFn: (url: string) => boolean, withBuiltinFrames?: boolean): Promise<HTMLElement> { const target = createTarget(); const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const stackTrace = createStackTrace([ 'USER_ID::userNestedFunction::http://example.com/script.js::40::15', 'USER_ID::userFunction::http://example.com/script.js::10::2', 'APP_ID::entry::http://example.com/app.js::25::10', ]); const stackTraceMessage = errorMessageForStack(stackTrace, withBuiltinFrames); const messageDetails = { type: Protocol.Runtime.ConsoleAPICalledEventType.Error, stackTrace, parameters: [{ type: 'object', subtype: 'error', className: 'Error', description: stackTraceMessage, } as Protocol.Runtime.RemoteObject], }; const rawMessage = new SDK.ConsoleModel.ConsoleMessage( runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, Protocol.Log.LogEntryLevel.Error, stackTraceMessage, messageDetails); const {message, linkifier} = createConsoleViewMessageWithStubDeps(rawMessage); linkifier.linkifyScriptLocation.callsFake((_target, _scriptId, sourceURL, lineNumber, options) => { const link = Components.Linkifier.Linkifier.linkifyURL(sourceURL, {lineNumber, ...options}); if (ignoreListFn(sourceURL)) { link.classList.add(IGNORE_LIST_LINK); } return link; }); linkifier.maybeLinkifyConsoleCallFrame.callsFake((_target, callFrame, options) => { const link = Components.Linkifier.Linkifier.linkifyURL( urlString`${callFrame.url}`, {lineNumber: callFrame.lineNumber, ...options}); if (ignoreListFn(callFrame.url)) { link.classList.add(IGNORE_LIST_LINK); } return link; }); const element = message.toMessageElement(); // Trigger rendering. await message.formatErrorStackPromiseForTest(); const wrapperElement = document.createElement('div'); const shadowElement = UI.UIUtils.createShadowRootWithCoreStyles(wrapperElement, {cssFile: consoleViewStyles}); shadowElement.appendChild(element); renderElementIntoDOM(wrapperElement); assert.isTrue(element.checkVisibility()); return element; } const EXPANDED_UNSTRUCTURED = [ ' at userNestedFunction (/script.js:40:15)\n', ' at userFunction (/script.js:10:2)\n', ' at entry (/app.js:25:10)', ]; const COLLAPSED_UNSTRUCTURED = [ ' at userNestedFunction (/script.js:40:15)\n', ' at userFunction (/script.js:10:2)\n', ]; const EXPANDED_UNSTRUCTURED_WITH_BUILTIN = [ ' at userNestedFunction (/script.js:40:15)\n', ' at JSON.parse (<anonymous>)\n', ' at userFunction (/script.js:10:2)\n', ' at JSON.parse (<anonymous>)\n', ' at entry (/app.js:25:10)\n', ' at JSON.parse (<anonymous>)', ]; const COLLAPSED_UNSTRUCTURED_WITH_BUILTIN = [ ' at userNestedFunction (/script.js:40:15)\n', ' at JSON.parse (<anonymous>)\n', ' at userFunction (/script.js:10:2)\n', ' at JSON.parse (<anonymous>)\n', ]; const EXPANDED_STRUCTURED = [ '\nuserNestedFunction @ example.com/script.js:41', '\nuserFunction @ example.com/script.js:11', '\nentry @ example.com/app.js:26', ]; const COLLAPSED_STRUCTURED = [ '\nuserNestedFunction @ example.com/script.js:41', '\nuserFunction @ example.com/script.js:11', ]; it('shows everything with no links when nothing is ignore listed', async () => { const element = await createConsoleMessageWithIgnoreListing(_ => false); assertNoLinks(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED); assert.deepEqual(getStructuredCallFrames(element), []); expandStructuredTrace(element); assertNoLinks(element); assert.deepEqual(getStructuredCallFrames(element), EXPANDED_STRUCTURED); }); it('shows everything with no links when everything is ignore listed', async () => { const element = await createConsoleMessageWithIgnoreListing(_ => true); assertNoLinks(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED); assert.deepEqual(getStructuredCallFrames(element), []); expandStructuredTrace(element); assertNoLinks(element); assert.deepEqual(getStructuredCallFrames(element), EXPANDED_STRUCTURED); }); it('shows expandable list when something is ignore listed', async () => { const element = await createConsoleMessageWithIgnoreListing(url => url.includes('/app.js')); assertShowAllLink(element); assert.deepEqual(getStructuredCallFrames(element), []); assert.deepEqual(getCallFrames(element), COLLAPSED_UNSTRUCTURED); expandIgnored(element); assertShowLessLink(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED); collapseIgnored(element); assertShowAllLink(element); expandStructuredTrace(element); assertShowAllLink(element); assert.deepEqual(getStructuredCallFrames(element), COLLAPSED_STRUCTURED); assert.deepEqual(getCallFrames(element), COLLAPSED_UNSTRUCTURED); expandIgnored(element); assertShowLessLink(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED); assert.deepEqual(getStructuredCallFrames(element), EXPANDED_STRUCTURED); collapseIgnored(element); assertShowAllLink(element); assert.deepEqual(getStructuredCallFrames(element), COLLAPSED_STRUCTURED); assert.deepEqual(getCallFrames(element), COLLAPSED_UNSTRUCTURED); }); it('shows everything with no links when nothing is ignore listed, including builtin frames', async () => { const element = await createConsoleMessageWithIgnoreListing(_ => false, true); assertNoLinks(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED_WITH_BUILTIN); assert.deepEqual(getStructuredCallFrames(element), []); expandStructuredTrace(element); assertNoLinks(element); assert.deepEqual(getStructuredCallFrames(element), EXPANDED_STRUCTURED); }); it('shows everything with no links when everything is ignore listed, including builtin frames', async () => { const element = await createConsoleMessageWithIgnoreListing(_ => true, true); assertNoLinks(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED_WITH_BUILTIN); assert.deepEqual(getStructuredCallFrames(element), []); expandStructuredTrace(element); assertNoLinks(element); assert.deepEqual(getStructuredCallFrames(element), EXPANDED_STRUCTURED); }); it('shows expandable list when something is ignore listed, collapsing builtin frames', async () => { const element = await createConsoleMessageWithIgnoreListing(url => url.includes('/app.js'), true); assertShowAllLink(element); assert.deepEqual(getStructuredCallFrames(element), []); assert.deepEqual(getCallFrames(element), COLLAPSED_UNSTRUCTURED_WITH_BUILTIN); expandIgnored(element); assertShowLessLink(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED_WITH_BUILTIN); collapseIgnored(element); assertShowAllLink(element); expandStructuredTrace(element); assertShowAllLink(element); assert.deepEqual(getStructuredCallFrames(element), COLLAPSED_STRUCTURED); assert.deepEqual(getCallFrames(element), COLLAPSED_UNSTRUCTURED_WITH_BUILTIN); expandIgnored(element); assertShowLessLink(element); assert.deepEqual(getCallFrames(element), EXPANDED_UNSTRUCTURED_WITH_BUILTIN); assert.deepEqual(getStructuredCallFrames(element), EXPANDED_STRUCTURED); collapseIgnored(element); assertShowAllLink(element); assert.deepEqual(getStructuredCallFrames(element), COLLAPSED_STRUCTURED); assert.deepEqual(getCallFrames(element), COLLAPSED_UNSTRUCTURED_WITH_BUILTIN); }); }); });