UNPKG

chrome-devtools-frontend

Version:
459 lines (412 loc) • 18.1 kB
// Copyright 2020 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 Protocol from '../../generated/protocol.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Workspace from '../../models/workspace/workspace.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import { describeWithMockConnection, dispatchEvent, setMockConnectionResponseHandler, } from '../../testing/MockConnection.js'; import {MockProtocolBackend} from '../../testing/MockScopeChain.js'; import * as Common from '../common/common.js'; import * as Platform from '../platform/platform.js'; import * as SDK from './sdk.js'; const {urlString} = Platform.DevToolsPath; const SCRIPT_ID_ONE = '1' as Protocol.Runtime.ScriptId; const SCRIPT_ID_TWO = '2' as Protocol.Runtime.ScriptId; describeWithMockConnection('DebuggerModel', () => { describe('breakpoint activation', () => { beforeEach(() => { // Dummy handlers for unblocking target suspension. setMockConnectionResponseHandler('Debugger.setAsyncCallStackDepth', () => ({})); setMockConnectionResponseHandler('Debugger.disable', () => ({})); setMockConnectionResponseHandler('DOM.disable', () => ({})); setMockConnectionResponseHandler('CSS.disable', () => ({})); setMockConnectionResponseHandler('Overlay.disable', () => ({})); setMockConnectionResponseHandler('Animation.disable', () => ({})); setMockConnectionResponseHandler('Overlay.setShowGridOverlays', () => ({})); setMockConnectionResponseHandler('Overlay.setShowFlexOverlays', () => ({})); setMockConnectionResponseHandler('Overlay.setShowScrollSnapOverlays', () => ({})); setMockConnectionResponseHandler('Overlay.setShowContainerQueryOverlays', () => ({})); setMockConnectionResponseHandler('Overlay.setShowIsolatedElements', () => ({})); setMockConnectionResponseHandler('Overlay.setShowViewportSizeOnResize', () => ({})); setMockConnectionResponseHandler('Target.setAutoAttach', () => ({})); // Dummy handlers for unblocking target resumption. setMockConnectionResponseHandler('Debugger.enable', () => ({})); setMockConnectionResponseHandler('Debugger.setPauseOnExceptions', () => ({})); setMockConnectionResponseHandler('DOM.enable', () => ({})); setMockConnectionResponseHandler('Overlay.enable', () => ({})); setMockConnectionResponseHandler('CSS.enable', () => ({})); setMockConnectionResponseHandler('Animation.enable', () => ({})); }); it('deactivates breakpoints on construction with inactive breakpoints', async () => { let breakpointsDeactivated = false; setMockConnectionResponseHandler('Debugger.setBreakpointsActive', request => { if (request.active === false) { breakpointsDeactivated = true; } return {}; }); Common.Settings.Settings.instance().moduleSetting('breakpoints-active').set(false); createTarget(); assert.isTrue(breakpointsDeactivated); }); it('deactivates breakpoints for suspended target', async () => { let breakpointsDeactivated = false; setMockConnectionResponseHandler('Debugger.setBreakpointsActive', request => { if (request.active === false) { breakpointsDeactivated = true; } return {}; }); const target = createTarget(); await target.suspend(); // Deactivate breakpoints while suspended. Common.Settings.Settings.instance().moduleSetting('breakpoints-active').set(false); // Verify that the backend received the message. assert.isTrue(breakpointsDeactivated); // Resume and verify that the setBreakpointsActive(false) is called again when the target resumes. // This is only needed for older backends (before crbug.com/1357046 is fixed). breakpointsDeactivated = false; await target.resume(); assert.isTrue(breakpointsDeactivated); }); it('activates breakpoints for suspended target', async () => { let breakpointsDeactivated = false; let breakpointsActivated = false; setMockConnectionResponseHandler('Debugger.setBreakpointsActive', request => { if (request.active) { breakpointsActivated = true; } else { breakpointsDeactivated = true; } return {}; }); // Deactivate breakpoints befroe the target is created. Common.Settings.Settings.instance().moduleSetting('breakpoints-active').set(false); const target = createTarget(); assert.isTrue(breakpointsDeactivated); await target.suspend(); // Activate breakpoints while suspended. Common.Settings.Settings.instance().moduleSetting('breakpoints-active').set(true); // Verify that the backend received the message. assert.isTrue(breakpointsActivated); }); }); describe('createRawLocationFromURL', () => { it('yields correct location in the presence of multiple scripts with the same URL', async () => { const target = createTarget(); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); const url = 'http://localhost/index.html'; dispatchEvent(target, 'Debugger.scriptParsed', { scriptId: SCRIPT_ID_ONE, url, startLine: 0, startColumn: 0, endLine: 1, endColumn: 10, executionContextId: 1, hash: '', isLiveEdit: false, sourceMapURL: undefined, hasSourceURL: false, length: 10, }); dispatchEvent(target, 'Debugger.scriptParsed', { scriptId: SCRIPT_ID_TWO, url, startLine: 20, startColumn: 0, endLine: 21, endColumn: 10, executionContextId: 1, hash: '', isLiveEdit: false, sourceMapURL: undefined, hasSourceURL: false, length: 10, }); assert.strictEqual(debuggerModel?.createRawLocationByURL(url, 0)?.scriptId, SCRIPT_ID_ONE); assert.strictEqual(debuggerModel?.createRawLocationByURL(url, 20, 1)?.scriptId, SCRIPT_ID_TWO); assert.isNull(debuggerModel?.createRawLocationByURL(url, 5, 5)); }); }); const breakpointId1 = 'fs.js:1' as Protocol.Debugger.BreakpointId; const breakpointId2 = 'unsupported' as Protocol.Debugger.BreakpointId; describe('setBreakpointByURL', () => { it('correctly sets only a single breakpoint in Node.js internal scripts', async () => { setMockConnectionResponseHandler('Debugger.setBreakpointByUrl', ({url}) => { if (url === 'fs.js') { return { breakpointId: breakpointId1, locations: [], getError() { return undefined; }, }; } return { breakpointId: breakpointId2, locations: [], getError() { return undefined; }, }; }); const target = createTarget(); target.markAsNodeJSForTest(); const model = new SDK.DebuggerModel.DebuggerModel(target); const {breakpointId} = await model.setBreakpointByURL(urlString`fs.js`, 1); assert.strictEqual(breakpointId, breakpointId1); }); }); describe('scriptsForSourceURL', () => { it('returns the latest script at the front of the result for scripts with the same URL', () => { const target = createTarget(); const url = 'http://localhost/index.html'; dispatchEvent(target, 'Debugger.scriptParsed', { scriptId: SCRIPT_ID_ONE, url, startLine: 0, startColumn: 0, endLine: 1, endColumn: 10, executionContextId: 1, hash: '', isLiveEdit: false, sourceMapURL: undefined, hasSourceURL: false, length: 10, }); dispatchEvent(target, 'Debugger.scriptParsed', { scriptId: SCRIPT_ID_TWO, url, startLine: 20, startColumn: 0, endLine: 21, endColumn: 10, executionContextId: 1, hash: '', isLiveEdit: false, sourceMapURL: undefined, hasSourceURL: false, length: 10, }); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); const scripts = debuggerModel?.scriptsForSourceURL(url) || []; assert.strictEqual(scripts[0].scriptId, SCRIPT_ID_TWO); assert.strictEqual(scripts[1].scriptId, SCRIPT_ID_ONE); }); }); describe('Scope', () => { it('Scope.typeName covers every enum value', async () => { const target = createTarget(); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel; const scriptUrl = urlString`https://script-host/script.js`; const script = new SDK.Script.Script( debuggerModel, SCRIPT_ID_ONE, scriptUrl, 0, 0, 0, 0, 0, '', false, false, undefined, false, 0, null, null, null, null, null, null); const scopeTypes: Protocol.Debugger.ScopeType[] = [ Protocol.Debugger.ScopeType.Global, Protocol.Debugger.ScopeType.Local, Protocol.Debugger.ScopeType.With, Protocol.Debugger.ScopeType.Closure, Protocol.Debugger.ScopeType.Catch, Protocol.Debugger.ScopeType.Block, Protocol.Debugger.ScopeType.Script, Protocol.Debugger.ScopeType.Eval, Protocol.Debugger.ScopeType.Module, Protocol.Debugger.ScopeType.WasmExpressionStack, ]; for (const scopeType of scopeTypes) { const payload: Protocol.Debugger.CallFrame = { callFrameId: '0' as Protocol.Debugger.CallFrameId, functionName: 'test', functionLocation: undefined, location: { scriptId: SCRIPT_ID_ONE, lineNumber: 0, columnNumber: 0, }, url: 'test-url', scopeChain: [{ type: scopeType, object: {type: 'object'} as Protocol.Runtime.RemoteObject, }], this: {type: 'object'} as Protocol.Runtime.RemoteObject, returnValue: undefined, canBeRestarted: false, }; const callFrame = new SDK.DebuggerModel.CallFrame(debuggerModel, script, payload, 0); const scope = new SDK.DebuggerModel.Scope(callFrame, 0); assert.notEqual('', scope.typeName()); } }); }); describe('pause', () => { let target: SDK.Target.Target; let backend: MockProtocolBackend; let debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding; beforeEach(() => { target = createTarget({id: 'main' as Protocol.Target.TargetID, name: 'main', type: SDK.Target.Type.FRAME}); const targetManager = target.targetManager(); const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance( {forceNew: false, resourceMapping, targetManager}); backend = new MockProtocolBackend(); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: false, debuggerWorkspaceBinding}); }); it('with empty call frame list will invoke plain step-into', async () => { const stepIntoRequestPromise = new Promise<void>(resolve => { setMockConnectionResponseHandler('Debugger.stepInto', () => { resolve(); return {}; }); }); backend.dispatchDebuggerPauseWithNoCallFrames(target, Protocol.Debugger.PausedEventReason.Other); await stepIntoRequestPromise; }); }); }); describe('DebuggerModel', () => { describe('selectSymbolSource', () => { const embeddedDwarfSymbols: Protocol.Debugger.DebugSymbols = {type: Protocol.Debugger.DebugSymbolsType.EmbeddedDWARF, externalURL: ''}; const externalDwarfSymbols: Protocol.Debugger.DebugSymbols = {type: Protocol.Debugger.DebugSymbolsType.ExternalDWARF, externalURL: 'abc'}; const sourceMapSymbols: Protocol.Debugger.DebugSymbols = {type: Protocol.Debugger.DebugSymbolsType.SourceMap, externalURL: 'abc'}; beforeEach(() => { Common.Console.Console.instance({forceNew: true}); }); function testSelectSymbolSource( debugSymbols: Protocol.Debugger.DebugSymbols[]|null, expectedSymbolType: Protocol.Debugger.DebugSymbolsType, expectedWarning?: string) { const selectedSymbol = SDK.DebuggerModel.DebuggerModel.selectSymbolSource(debugSymbols); assert.isNotNull(selectedSymbol); assert.strictEqual(selectedSymbol.type, expectedSymbolType); const consoleMessages = Common.Console.Console.instance().messages(); if (!expectedWarning) { assert.lengthOf(consoleMessages, 0); return; } assert.lengthOf(consoleMessages, 1); assert.deepEqual(consoleMessages[0].text, expectedWarning); } it('prioritizes external DWARF over all types', () => { const debugSymbols = [embeddedDwarfSymbols, externalDwarfSymbols, sourceMapSymbols]; const expectedSelectedSymbol = Protocol.Debugger.DebugSymbolsType.ExternalDWARF; const expectedWarning = 'Multiple debug symbols for script were found. Using ExternalDWARF'; testSelectSymbolSource(debugSymbols, expectedSelectedSymbol, expectedWarning); }); it('prioritizes embedded DWARF if source maps and embedded DWARF exist', () => { const debugSymbols = [embeddedDwarfSymbols, sourceMapSymbols]; const expectedSymbolType = Protocol.Debugger.DebugSymbolsType.EmbeddedDWARF; const expectedWarning = 'Multiple debug symbols for script were found. Using EmbeddedDWARF'; testSelectSymbolSource(debugSymbols, expectedSymbolType, expectedWarning); }); it('picks source maps if no DWARF is available', () => { const debugSymbols = [sourceMapSymbols]; const expectedSymbolType = Protocol.Debugger.DebugSymbolsType.SourceMap; testSelectSymbolSource(debugSymbols, expectedSymbolType); }); it('returns null if nothing is available', () => { const selectedSymbol = SDK.DebuggerModel.DebuggerModel.selectSymbolSource([]); assert.isNull(selectedSymbol); const consoleMessages = Common.Console.Console.instance().messages(); assert.lengthOf(consoleMessages, 0); }); }); describe('sortAndMergeRanges', () => { function createRange( scriptId: Protocol.Runtime.ScriptId, startLine: number, startColumn: number, endLine: number, endColumn: number): Protocol.Debugger.LocationRange { return { scriptId, start: {lineNumber: startLine, columnNumber: startColumn}, end: {lineNumber: endLine, columnNumber: endColumn}, }; } function sortAndMerge(locationRange: Protocol.Debugger.LocationRange[]) { return SDK.DebuggerModel.sortAndMergeRanges(locationRange.concat()); } function assertIsMaximallyMerged(locationRange: Protocol.Debugger.LocationRange[]) { for (let i = 1; i < locationRange.length; ++i) { const prev = locationRange[i - 1]; const curr = locationRange[i]; assert.isTrue(prev.scriptId <= curr.scriptId); if (prev.scriptId === curr.scriptId) { assert.isTrue(prev.end.lineNumber <= curr.start.lineNumber); if (prev.end.lineNumber === curr.start.lineNumber) { assert.isTrue(prev.end.columnNumber <= curr.start.columnNumber); } } } } it('can be reduced if equal', () => { const testRange = createRange(SCRIPT_ID_ONE, 0, 3, 3, 3); const locationRangesToBeReduced = [ testRange, testRange, ]; const reduced = sortAndMerge(locationRangesToBeReduced); assert.deepEqual(reduced, [testRange]); assertIsMaximallyMerged(reduced); }); it('can be reduced if overlapping (multiple ranges)', () => { const locationRangesToBeReduced = [ createRange(SCRIPT_ID_ONE, 0, 5, 5, 3), createRange(SCRIPT_ID_ONE, 0, 3, 3, 3), createRange(SCRIPT_ID_ONE, 5, 3, 10, 10), createRange(SCRIPT_ID_TWO, 5, 4, 10, 10), ]; const locationRangesExpected = [ createRange(SCRIPT_ID_ONE, 0, 3, 10, 10), locationRangesToBeReduced[3], ]; const reduced = sortAndMerge(locationRangesToBeReduced); assert.deepEqual(reduced, locationRangesExpected); assertIsMaximallyMerged(reduced); }); it('can be reduced if overlapping (same start, different end)', () => { const locationRangesToBeReduced = [ createRange(SCRIPT_ID_ONE, 0, 5, 5, 3), createRange(SCRIPT_ID_ONE, 0, 5, 3, 3), ]; const locationRangesExpected = [ createRange(SCRIPT_ID_ONE, 0, 5, 5, 3), ]; const reduced = sortAndMerge(locationRangesToBeReduced); assert.deepEqual(reduced, locationRangesExpected); assertIsMaximallyMerged(reduced); }); it('can be reduced if overlapping (different start, same end)', () => { const locationRangesToBeReduced = [ createRange(SCRIPT_ID_ONE, 0, 3, 5, 3), createRange(SCRIPT_ID_ONE, 0, 5, 5, 3), ]; const locationRangesExpected = [ createRange(SCRIPT_ID_ONE, 0, 3, 5, 3), ]; const reduced = sortAndMerge(locationRangesToBeReduced); assert.deepEqual(reduced, locationRangesExpected); assertIsMaximallyMerged(reduced); }); it('can be reduced if overlapping (start == other.end)', () => { const locationRangesToBeReduced = [ createRange(SCRIPT_ID_ONE, 0, 3, 5, 3), createRange(SCRIPT_ID_ONE, 5, 3, 10, 3), ]; const locationRangesExpected = [ createRange(SCRIPT_ID_ONE, 0, 3, 10, 3), ]; const reduced = sortAndMerge(locationRangesToBeReduced); assert.deepEqual(reduced, locationRangesExpected); assertIsMaximallyMerged(reduced); }); }); });