UNPKG

chrome-devtools-frontend

Version:
1,108 lines (931 loc) • 85.9 kB
// Copyright 2022 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 type {Chrome} from '../../../extension-api/ExtensionAPI.js'; import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import {TestPlugin} from '../../testing/LanguagePluginHelpers.js'; import { clearMockConnectionResponseHandler, describeWithMockConnection, dispatchEvent, registerListenerOnOutgoingMessage, setMockConnectionResponseHandler, } from '../../testing/MockConnection.js'; import {MockProtocolBackend} from '../../testing/MockScopeChain.js'; import {createFileSystemFileForPersistenceTests} from '../../testing/PersistenceHelpers.js'; import {getInitializedResourceTreeModel} from '../../testing/ResourceTreeHelpers.js'; import {encodeSourceMap} from '../../testing/SourceMapEncoder.js'; import {setupPageResourceLoaderForSourceMap} from '../../testing/SourceMapHelpers.js'; import { createContentProviderUISourceCode, } from '../../testing/UISourceCodeHelpers.js'; import * as Bindings from '../bindings/bindings.js'; import * as Breakpoints from '../breakpoints/breakpoints.js'; import * as Persistence from '../persistence/persistence.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as Workspace from '../workspace/workspace.js'; const {urlString} = Platform.DevToolsPath; describeWithMockConnection('BreakpointManager', () => { const URL_HTML = urlString`http://site/index.html`; const INLINE_SCRIPT_START = 41; const BREAKPOINT_SCRIPT_LINE = 1; const INLINE_BREAKPOINT_RAW_LINE = BREAKPOINT_SCRIPT_LINE + INLINE_SCRIPT_START; const BREAKPOINT_RESULT_COLUMN = 5; const inlineScriptDescription = { url: URL_HTML, content: 'console.log(1);\nconsole.log(2);\n', startLine: INLINE_SCRIPT_START, startColumn: 0, hasSourceURL: false, embedderName: URL_HTML, }; const URL = urlString`http://site/script.js`; const scriptDescription = { url: URL, content: 'console.log(1);\nconsole.log(2);\n', startLine: 0, startColumn: 0, hasSourceURL: false, }; const DEFAULT_BREAKPOINT: [Breakpoints.BreakpointManager.UserCondition, boolean, boolean, Breakpoints.BreakpointManager.BreakpointOrigin] = [ Breakpoints.BreakpointManager.EMPTY_BREAKPOINT_CONDITION, true, // enabled false, // isLogpoint Breakpoints.BreakpointManager.BreakpointOrigin.OTHER, ]; // For tests with source maps. const ORIGINAL_SCRIPT_SOURCES_CONTENT = 'function foo() {\n console.log(\'Hello\');\n}\n'; const COMPILED_SCRIPT_SOURCES_CONTENT = 'function foo(){console.log("Hello")}'; const SOURCE_MAP_URL = urlString`https://site/script.js.map`; const ORIGINAL_SCRIPT_SOURCE_URL = urlString`https://site/original-script.js`; // Created with `terser -m -o script.min.js --source-map "includeSources;url=script.min.js.map" original-script.js` const sourceMapContent = JSON.stringify({ version: 3, names: ['foo', 'console', 'log'], sources: ['/original-script.js'], sourcesContent: [ORIGINAL_SCRIPT_SOURCES_CONTENT], mappings: 'AAAA,SAASA,MACPC,QAAQC,IAAI,QACd', }); let target: SDK.Target.Target; let backend: MockProtocolBackend; let breakpointManager: Breakpoints.BreakpointManager.BreakpointManager; let debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding; let targetManager: SDK.TargetManager.TargetManager; let workspace: Workspace.Workspace.WorkspaceImpl; beforeEach(async () => { workspace = Workspace.Workspace.WorkspaceImpl.instance(); targetManager = SDK.TargetManager.TargetManager.instance(); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); backend = new MockProtocolBackend(); target = createTarget(); SDK.TargetManager.TargetManager.instance().setScopeTarget(target); // Wait for the resource tree model to load; otherwise, our uiSourceCodes could be asynchronously // invalidated during the test. await getInitializedResourceTreeModel(target); breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); }); async function uiSourceCodeFromScript(debuggerModel: SDK.DebuggerModel.DebuggerModel, script: SDK.Script.Script): Promise<Workspace.UISourceCode.UISourceCode|null> { const rawLocation = debuggerModel.createRawLocation(script, 0, 0); const uiLocation = await breakpointManager.debuggerWorkspaceBinding.rawLocationToUILocation(rawLocation); return uiLocation?.uiSourceCode ?? null; } describe('possibleBreakpoints', () => { it('correctly asks the back-end for breakable positions', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); // Create an inline script and get a UI source code instance for it. const script = await backend.addScript(target, scriptDescription, null); const {scriptId} = script; const uiSourceCode = await uiSourceCodeFromScript(debuggerModel, script); assert.exists(uiSourceCode); function getPossibleBreakpointsStub(_request: Protocol.Debugger.GetPossibleBreakpointsRequest): Protocol.Debugger.GetPossibleBreakpointsResponse { return { locations: [ {scriptId, lineNumber: 0, columnNumber: 4}, {scriptId, lineNumber: 0, columnNumber: 8}, ], getError() { return undefined; }, }; } const getPossibleBreakpoints = sinon.spy(getPossibleBreakpointsStub); setMockConnectionResponseHandler('Debugger.getPossibleBreakpoints', getPossibleBreakpoints); const uiTextRange = new TextUtils.TextRange.TextRange(0, 0, 1, 0); const possibleBreakpoints = await breakpointManager.possibleBreakpoints(uiSourceCode, uiTextRange); assert.lengthOf(possibleBreakpoints, 2); assert.strictEqual(possibleBreakpoints[0].uiSourceCode, uiSourceCode); assert.strictEqual(possibleBreakpoints[0].lineNumber, 0); assert.strictEqual(possibleBreakpoints[0].columnNumber, 4); assert.strictEqual(possibleBreakpoints[1].uiSourceCode, uiSourceCode); assert.strictEqual(possibleBreakpoints[1].lineNumber, 0); assert.strictEqual(possibleBreakpoints[1].columnNumber, 8); assert.isTrue(getPossibleBreakpoints.calledOnceWith(sinon.match({ start: { scriptId, lineNumber: 0, columnNumber: 0, }, end: { scriptId, lineNumber: 1, columnNumber: 0, }, restrictToFunction: false, }))); }); }); describe('Breakpoints', () => { it('are removed and kept in storage after a back-end error', async () => { // Simulates a back-end error. const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); if (!debuggerModel.isReadyToPause()) { await debuggerModel.once(SDK.DebuggerModel.Events.DebuggerIsReadyToPause); } // Create an inline script and get a UI source code instance for it. const script = await backend.addScript(target, scriptDescription, null); const uiSourceCode = await uiSourceCodeFromScript(debuggerModel, script); assert.exists(uiSourceCode); // Set up the backend to respond with an error. backend.setBreakpointByUrlToFail(URL, BREAKPOINT_SCRIPT_LINE); // Set the breakpoint. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, BREAKPOINT_SCRIPT_LINE, 2, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); const removedSpy = sinon.spy(breakpoint, 'remove'); await breakpoint.updateBreakpoint(); // Breakpoint was removed and is kept in storage. assert.isTrue(breakpoint.getIsRemoved()); sinon.assert.calledWith(removedSpy, true); }); it('are only set if the uiSourceCode is still valid (not removed)', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); // Add a script. const script = await backend.addScript(target, scriptDescription, null); const uiSourceCode = await uiSourceCodeFromScript(debuggerModel, script); assert.exists(uiSourceCode); // Remove the project (and thus the uiSourceCode). Workspace.Workspace.WorkspaceImpl.instance().removeProject(uiSourceCode.project()); // Set the breakpoint. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, BREAKPOINT_SCRIPT_LINE, 2, ...DEFAULT_BREAKPOINT); // We should not expect any breakpoints to be set. assert.isUndefined(breakpoint); const breakLocations = breakpointManager.allBreakpointLocations(); assert.lengthOf(breakLocations, 0); }); }); describe('Breakpoint#backendCondition()', () => { function createBreakpoint(condition: string, isLogpoint: boolean): Breakpoints.BreakpointManager.Breakpoint { const {uiSourceCode} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'}); const storageState = { url: URL, resourceTypeName: uiSourceCode.contentType().name(), lineNumber: 5, condition: condition as Breakpoints.BreakpointManager.UserCondition, enabled: true, isLogpoint, }; return new Breakpoints.BreakpointManager.Breakpoint( breakpointManager, uiSourceCode, storageState, Breakpoints.BreakpointManager.BreakpointOrigin.USER_ACTION); } it('wraps logpoints in console.log', () => { const breakpoint = createBreakpoint('x', /* isLogpoint */ true); assert.include(breakpoint.backendCondition(), 'console.log(x)'); }); it('leaves conditional breakpoints alone', () => { const breakpoint = createBreakpoint('x === 42', /* isLogpoint */ false); // Split of sourceURL. const lines = breakpoint.backendCondition().split('\n'); assert.strictEqual(lines[0], 'x === 42'); }); it('has a sourceURL for logpoints', () => { const breakpoint = createBreakpoint('x', /* isLogpoint */ true); assert.include(breakpoint.backendCondition(), '//# sourceURL='); }); it('has a sourceURL for conditional breakpoints', () => { const breakpoint = createBreakpoint('x === 42', /* isLogpoint */ false); assert.include(breakpoint.backendCondition(), '//# sourceURL='); }); it('has no sourceURL for normal breakpoints', () => { const breakpoint = createBreakpoint('', /* isLogpoint */ false); assert.notInclude(breakpoint.backendCondition(), '//# sourceURL='); }); it('substitutes source-mapped variables', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const scriptInfo = {url: URL, content: 'function adder(n,r){const t=n+r;return t}'}; // Created with `terser -m -o script.min.js --source-map "includeSources;url=script.min.js.map" original-script.js` const sourceMapContent = JSON.stringify({ version: 3, names: ['adder', 'param1', 'param2', 'result'], sources: ['/original-script.js'], sourcesContent: ['function adder(param1, param2) {\n const result = param1 + param2;\n return result;\n}\n\n'], mappings: 'AAAA,SAASA,MAAMC,EAAQC,GACrB,MAAMC,EAASF,EAASC,EACxB,OAAOC,CACT', }); const sourceMapInfo = {url: SOURCE_MAP_URL, content: sourceMapContent}; const script = await backend.addScript(target, scriptInfo, sourceMapInfo); // Get the uiSourceCode for the original source. const uiSourceCode = await debuggerWorkspaceBinding.uiSourceCodeForSourceMapSourceURLPromise( debuggerModel, ORIGINAL_SCRIPT_SOURCE_URL, script.isContentScript()); assert.exists(uiSourceCode); // Mock out "Debugger.setBreakpointByUrl and just echo back the request". const cdpSetBreakpointPromise = new Promise<Protocol.Debugger.SetBreakpointByUrlRequest>(res => { clearMockConnectionResponseHandler('Debugger.setBreakpointByUrl'); setMockConnectionResponseHandler('Debugger.setBreakpointByUrl', request => { res(request); return {}; }); }); // Set the breakpoint on the `const result = ...` line with a condition using // "authored" variable names. const breakpoint = await breakpointManager.setBreakpoint( uiSourceCode, 1, 0, 'param1 > 0' as Breakpoints.BreakpointManager.UserCondition, /* enabled */ true, /* isLogpoint */ false, Breakpoints.BreakpointManager.BreakpointOrigin.USER_ACTION); assert.exists(breakpoint); await breakpoint.updateBreakpoint(); const {url, lineNumber, columnNumber, condition} = await cdpSetBreakpointPromise; assert.strictEqual(url, URL); assert.strictEqual(lineNumber, 0); assert.strictEqual(columnNumber, 20); assert.strictEqual(condition, 'n > 0\n\n//# sourceURL=debugger://breakpoint'); }); }); it('substitutes source-mapped variables for the same original script in different bundles correctly', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); // Create two 'bundles' that are identical modulo variable names. const url1 = urlString`http://site/script1.js`; const url2 = urlString`http://site/script2.js`; const scriptInfo1 = {url: url1, content: 'function adder(n,r){const t=n+r;return t}'}; const scriptInfo2 = {url: url2, content: 'function adder(o,p){const t=o+p;return t}'}; // The source map is the same for both 'bundles'. // Created with `terser -m -o script.min.js --source-map "includeSources;url=script.min.js.map" original-script.js` const sourceMapContent = JSON.stringify({ version: 3, names: ['adder', 'param1', 'param2', 'result'], sources: ['/original-script.js'], sourcesContent: ['function adder(param1, param2) {\n const result = param1 + param2;\n return result;\n}\n\n'], mappings: 'AAAA,SAASA,MAAMC,EAAQC,GACrB,MAAMC,EAASF,EAASC,EACxB,OAAOC,CACT', }); const sourceMapInfo = {url: SOURCE_MAP_URL, content: sourceMapContent}; await Promise.all([ backend.addScript(target, scriptInfo1, sourceMapInfo), backend.addScript(target, scriptInfo2, sourceMapInfo), ]); // Get the uiSourceCode for the original source. const uiSourceCode = await debuggerWorkspaceBinding.uiSourceCodeForSourceMapSourceURLPromise( debuggerModel, ORIGINAL_SCRIPT_SOURCE_URL, /* isContentScript */ false); assert.exists(uiSourceCode); // Mock out "Debugger.setBreakpointByUrl and echo back the first two 'Debugger.setBreakpointByUrl' requests. const cdpSetBreakpointPromise = new Promise<Map<string, Protocol.Debugger.SetBreakpointByUrlRequest>>(res => { clearMockConnectionResponseHandler('Debugger.setBreakpointByUrl'); const requests = new Map<string, Protocol.Debugger.SetBreakpointByUrlRequest>(); setMockConnectionResponseHandler('Debugger.setBreakpointByUrl', request => { requests.set(request.url, request); if (requests.size === 2) { res(requests); } return {}; }); }); // Set the breakpoint on the `const result = ...` line with a condition using // "authored" variable names. const breakpoint = await breakpointManager.setBreakpoint( uiSourceCode, 1, 0, 'param1 > 0' as Breakpoints.BreakpointManager.UserCondition, /* enabled */ true, /* isLogpoint */ false, Breakpoints.BreakpointManager.BreakpointOrigin.USER_ACTION); assert.exists(breakpoint); await breakpoint.updateBreakpoint(); const requests = await cdpSetBreakpointPromise; const req1 = requests.get(url1); assert.exists(req1); assert.strictEqual(req1.url, url1); assert.strictEqual(req1.condition, 'n > 0\n\n//# sourceURL=debugger://breakpoint'); const req2 = requests.get(url2); assert.exists(req2); assert.strictEqual(req2.url, url2); assert.strictEqual(req2.condition, 'o > 0\n\n//# sourceURL=debugger://breakpoint'); }); it('allows awaiting the restoration of breakpoints', async () => { Root.Runtime.experiments.enableForTest(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const {uiSourceCode, project} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'}); const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, 0, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Make sure that we await all updates that are triggered by adding the model. await breakpoint.updateBreakpoint(); const responder = backend.responderToBreakpointByUrlRequest(URL, 0); const script = await backend.addScript(target, scriptDescription, null); void responder({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: script.scriptId, lineNumber: 0, columnNumber: 9, }, ], }); // Retrieve the ModelBreakpoint that is linked to our DebuggerModel. const modelBreakpoint = breakpoint.modelBreakpoint(debuggerModel); assert.exists(modelBreakpoint); // Make sure that we do not have a linked script yet. // eslint-disable-next-line rulesdir/no-assert-equal-boolean-null-undefined assert.strictEqual(modelBreakpoint.currentState, null); // Now await restoring the breakpoint. // A successful restore should update the ModelBreakpoint of the DebuggerModel // to reflect a state, in which we have successfully set a breakpoint (i.e. a script id // is available). await breakpointManager.restoreBreakpointsForScript(script); assert.isNotNull(modelBreakpoint.currentState); assert.lengthOf(modelBreakpoint.currentState, 1); assert.strictEqual(modelBreakpoint.currentState[0].url, URL); // Clean up. await breakpoint.remove(false); Workspace.Workspace.WorkspaceImpl.instance().removeProject(project); Root.Runtime.experiments.disableForTest(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS); }); it('allows awaiting on scheduled update in debugger', async () => { Root.Runtime.experiments.enableForTest(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const {uiSourceCode, project} = createContentProviderUISourceCode({url: URL, mimeType: 'text/javascript'}); const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, 13, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Make sure that we await all updates that are triggered by adding the model. await breakpoint.updateBreakpoint(); const responder = backend.responderToBreakpointByUrlRequest(URL, 13); const script = await backend.addScript(target, scriptDescription, null); void responder({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: script.scriptId, lineNumber: 13, columnNumber: 9, }, ], }); // Retrieve the ModelBreakpoint that is linked to our DebuggerModel. const modelBreakpoint = breakpoint.modelBreakpoint(debuggerModel); assert.exists(modelBreakpoint); assert.isNull(breakpoint.getLastResolvedState()); const update = modelBreakpoint.scheduleUpdateInDebugger(); assert.isNull(breakpoint.getLastResolvedState()); const result = await update; // Make sure that no error occurred. assert.strictEqual(result, Breakpoints.BreakpointManager.DebuggerUpdateResult.OK); assert.strictEqual(breakpoint.getLastResolvedState()?.[0].lineNumber, 13); await breakpoint.remove(false); Workspace.Workspace.WorkspaceImpl.instance().removeProject(project); }); it('allows awaiting on removal of breakpoint in debugger', async () => { Root.Runtime.experiments.enableForTest(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const script = await backend.addScript(target, scriptDescription, null); const uiSourceCode = await uiSourceCodeFromScript(debuggerModel, script); assert.exists(uiSourceCode); const breakpointId = 'BREAK_ID' as Protocol.Debugger.BreakpointId; void backend.responderToBreakpointByUrlRequest(URL, 13)({ breakpointId, locations: [ { scriptId: script.scriptId, lineNumber: 13, columnNumber: 9, }, ], }); const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, 13, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); await breakpoint.updateBreakpoint(); // Retrieve the ModelBreakpoint that is linked to our DebuggerModel. const modelBreakpoint = breakpoint.modelBreakpoint(debuggerModel); assert.exists(modelBreakpoint); assert.exists(modelBreakpoint.currentState); // Test if awaiting breakpoint.remove is actually removing the state. const removalPromise = backend.breakpointRemovedPromise(breakpointId); await breakpoint.remove(false); await removalPromise; assert.isNull(modelBreakpoint.currentState); }); it('removes ui source code from breakpoint after breakpoint live location update', async () => { const compiledScript = 'script.min.js'; const sourceRoot = 'https://site/'; const compiledScriptURL = sourceRoot + compiledScript; const scriptInfo = {url: compiledScriptURL, content: COMPILED_SCRIPT_SOURCES_CONTENT}; const sourceMapInfo = {url: SOURCE_MAP_URL, content: sourceMapContent, sourceRoot, sources: 'original-script.js'}; const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const uiSourceCodePromise = debuggerWorkspaceBinding.waitForUISourceCodeAdded(urlString`${compiledScriptURL}`, target); const script = await backend.addScript(target, scriptInfo, sourceMapInfo); const uiSourceCodeTs = await uiSourceCodeFromScript(debuggerModel, script); const uiSourceCode = await uiSourceCodePromise; assert.exists(uiSourceCodeTs); assert.exists(uiSourceCode); // Register our interest in the breakpoint request. const breakpointResponder = backend.responderToBreakpointByUrlRequest(compiledScriptURL, 0); // Set the breakpoint on the compiled script. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, 0, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Await the breakpoint request at the mock backend and send a CDP response once the request arrives. // Concurrently, enforce update of the breakpoint in the debugger. await Promise.all([ breakpointResponder({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ {scriptId: script.scriptId, lineNumber: BREAKPOINT_SCRIPT_LINE, columnNumber: BREAKPOINT_RESULT_COLUMN}, ], }), breakpoint.refreshInDebugger(), ]); // Verify that the location of the breakpoint is tied to the original script. assert.lengthOf(breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode), 0); assert.lengthOf(breakpointManager.breakpointLocationsForUISourceCode(uiSourceCodeTs), 1); assert.strictEqual(breakpointManager.breakpointLocationsForUISourceCode(uiSourceCodeTs)[0].breakpoint, breakpoint); assert.strictEqual( breakpointManager.breakpointLocationsForUISourceCode(uiSourceCodeTs)[0].uiLocation.lineNumber, 2); // Remove the target and verify that the UI source codes were removed from the breakpoint. breakpointManager.targetManager.removeTarget(target); assert.strictEqual(breakpoint.getUiSourceCodes().size, 0); assert.lengthOf(breakpointManager.breakpointLocationsForUISourceCode(uiSourceCodeTs), 0); assert.lengthOf(breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode), 0); await breakpoint.remove(false); }); it('can set breakpoints in inline scripts', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); // Create an inline script and get a UI source code instance for it. const inlineScript = await backend.addScript(target, inlineScriptDescription, null); const uiSourceCode = await uiSourceCodeFromScript(debuggerModel, inlineScript); assert.exists(uiSourceCode); // Register our interest in the breakpoint request. const breakpointResponder = backend.responderToBreakpointByUrlRequest(URL_HTML, INLINE_BREAKPOINT_RAW_LINE); // Set the breakpoint. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, BREAKPOINT_SCRIPT_LINE, 2, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Await the breakpoint request at the mock backend and send a CDP response once the request arrives. // Concurrently, enforce update of the breakpoint in the debugger. await Promise.all([ breakpointResponder({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: inlineScript.scriptId, lineNumber: INLINE_BREAKPOINT_RAW_LINE, columnNumber: BREAKPOINT_RESULT_COLUMN, }, ], }), breakpoint.refreshInDebugger(), ]); // Check that the breakpoint was set at the correct location? const locations = breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode); assert.lengthOf(locations, 1); assert.strictEqual(1, locations[0].uiLocation.lineNumber); assert.strictEqual(5, locations[0].uiLocation.columnNumber); }); it('can restore breakpoints in inline scripts', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); // Create an inline script and get a UI source code instance for it. const inlineScript = await backend.addScript(target, inlineScriptDescription, null); const uiSourceCode = await uiSourceCodeFromScript(debuggerModel, inlineScript); assert.exists(uiSourceCode); // Register our interest in the breakpoint request. const breakpointResponder = backend.responderToBreakpointByUrlRequest(URL_HTML, INLINE_BREAKPOINT_RAW_LINE); // Set the breakpoint on the front-end/model side. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, BREAKPOINT_SCRIPT_LINE, 2, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); assert.deepEqual(Array.from(breakpoint.getUiSourceCodes()), [uiSourceCode]); // Await the breakpoint request at the mock backend and send a CDP response once the request arrives. // Concurrently, enforce update of the breakpoint in the debugger. await Promise.all([ breakpointResponder({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: inlineScript.scriptId, lineNumber: INLINE_BREAKPOINT_RAW_LINE, columnNumber: BREAKPOINT_RESULT_COLUMN, }, ], }), breakpoint.refreshInDebugger(), ]); // Disconnect from the target. This will also unload the script. breakpointManager.targetManager.removeTarget(target); // Make sure the source code for the script was removed from the breakpoint. assert.strictEqual(breakpoint.getUiSourceCodes().size, 0); // Create a new target. target = createTarget(); SDK.TargetManager.TargetManager.instance().setScopeTarget(target); const reloadedDebuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(reloadedDebuggerModel); // Load the same inline script (with a different script id!) into the new target. // Once the model loads the script, it wil try to restore the breakpoint. Let us make sure the backend // will be ready to produce a response before adding the script. const reloadedBreakpointResponder = backend.responderToBreakpointByUrlRequest(URL_HTML, INLINE_BREAKPOINT_RAW_LINE); const reloadedInlineScript = await backend.addScript(target, inlineScriptDescription, null); const reloadedUiSourceCode = await uiSourceCodeFromScript(reloadedDebuggerModel, reloadedInlineScript); assert.exists(reloadedUiSourceCode); // Verify the breakpoint was restored at the oriignal unbound location (before the backend binds it). const unboundLocations = breakpointManager.breakpointLocationsForUISourceCode(reloadedUiSourceCode); assert.lengthOf(unboundLocations, 1); assert.strictEqual(1, unboundLocations[0].uiLocation.lineNumber); assert.strictEqual(2, unboundLocations[0].uiLocation.columnNumber); // Wait for the breakpoint request for the reloaded script and for the breakpoint update. await Promise.all([ reloadedBreakpointResponder({ breakpointId: 'RELOADED_BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [{ scriptId: reloadedInlineScript.scriptId, lineNumber: INLINE_BREAKPOINT_RAW_LINE, columnNumber: BREAKPOINT_RESULT_COLUMN, }], }), breakpoint.refreshInDebugger(), ]); // Verify the restored position. const boundLocations = breakpointManager.breakpointLocationsForUISourceCode(reloadedUiSourceCode); assert.lengthOf(boundLocations, 1); assert.strictEqual(1, boundLocations[0].uiLocation.lineNumber); assert.strictEqual(5, boundLocations[0].uiLocation.columnNumber); }); it('eagerly restores JavaScript breakpoints in a new target', async () => { // Remove the default target so that we can simulate starting the debugger afresh. targetManager.removeTarget(target); // Set the breakpoint storage to contain a breakpoint and re-initialize // the breakpoint manager from that storage. This should create a breakpoint instance // in the breakpoint manager. const url = urlString`http://example.com/script.js`; const lineNumber = 1; const breakpoints: Breakpoints.BreakpointManager.BreakpointStorageState[] = [{ url, resourceTypeName: 'script', lineNumber, condition: '' as Breakpoints.BreakpointManager.UserCondition, enabled: true, isLogpoint: false, }]; Common.Settings.Settings.instance().createLocalSetting('breakpoints', breakpoints).set(breakpoints); Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); // Create a new target and make sure that the backend receives setBreakpointByUrl request // from breakpoint manager. const breakpointSetPromise = backend.responderToBreakpointByUrlRequest(url, lineNumber)({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [], }); SDK.TargetManager.TargetManager.instance().setScopeTarget(createTarget()); await breakpointSetPromise; }); it('eagerly restores TypeScript breakpoints in a new target', async () => { // Remove the default target so that we can simulate starting the debugger afresh. targetManager.removeTarget(target); // Set the breakpoint storage to contain a source-mapped breakpoint and re-initialize // the breakpoint manager from that storage. This should create a breakpoint instance // in the breakpoint manager (for the resolved location!). const compiledUrl = urlString`http://example.com/compiled.js`; const compiledLineNumber = 2; const breakpoints: Breakpoints.BreakpointManager.BreakpointStorageState[] = [{ url: urlString`http://example.com/src/script.ts`, resourceTypeName: 'sm-script', lineNumber: 1, condition: '' as Breakpoints.BreakpointManager.UserCondition, enabled: true, isLogpoint: false, resolvedState: [{ url: compiledUrl, lineNumber: compiledLineNumber, columnNumber: 0, condition: '' as SDK.DebuggerModel.BackendCondition, }], }]; Common.Settings.Settings.instance().createLocalSetting('breakpoints', breakpoints).set(breakpoints); Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); // Create a new target and make sure that the backend receives setBreakpointByUrl request // from breakpoint manager. const breakpointSetPromise = backend.responderToBreakpointByUrlRequest(compiledUrl, compiledLineNumber)({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [], }); SDK.TargetManager.TargetManager.instance().setScopeTarget(createTarget()); await breakpointSetPromise; }); it('saves generated location into storage', async () => { // Remove the default target so that we can simulate starting the debugger afresh. targetManager.removeTarget(target); // Re-create a target and breakpoint manager. target = createTarget(); SDK.TargetManager.TargetManager.instance().setScopeTarget(target); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const breakpoints: Breakpoints.BreakpointManager.BreakpointStorageState[] = []; const setting = Common.Settings.Settings.instance().createLocalSetting('breakpoints', breakpoints); Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); // Add script with source map. setupPageResourceLoaderForSourceMap(sourceMapContent); const scriptInfo = {url: URL, content: COMPILED_SCRIPT_SOURCES_CONTENT}; const sourceMapInfo = {url: SOURCE_MAP_URL, content: sourceMapContent}; const script = await backend.addScript(target, scriptInfo, sourceMapInfo); // Get the uiSourceCode for the original source. const uiSourceCode = await debuggerWorkspaceBinding.uiSourceCodeForSourceMapSourceURLPromise( debuggerModel, ORIGINAL_SCRIPT_SOURCE_URL, script.isContentScript()); assert.exists(uiSourceCode); // Set the breakpoint on the front-end/model side. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, 1, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Set the breakpoint response for our upcoming request. void backend.responderToBreakpointByUrlRequest(URL, 0)({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: script.scriptId, lineNumber: 0, columnNumber: 15, }, ], }); // Ensure the breakpoint is fully set. await breakpoint.refreshInDebugger(); // Check that the storage contains the resolved breakpoint location. assert.lengthOf(setting.get(), 1); assert.deepEqual(setting.get()[0].resolvedState, [{ url: URL, lineNumber: 0, columnNumber: 15, condition: '' as SDK.DebuggerModel.BackendCondition, }]); }); it('restores latest breakpoints from storage', async () => { // Remove the default target so that we can simulate starting the debugger afresh. targetManager.removeTarget(target); const expectedBreakpointLines = [1, 2]; const breakpointRequestLines = new Promise<number[]>((resolve, reject) => { const breakpoints: Breakpoints.BreakpointManager.BreakpointStorageState[] = []; // Accumulator for breakpoint lines from setBreakpointByUrl requests. const breakpointRequestLinesReceived = new Set<number>(); // Create three breakpoints in the storage and register the corresponding // request handler in the mock backend. The handler will resolve the promise // (and thus finish up the test) once it receives two breakpoint requests. // The idea is to check that the front-end requested the two latest breakpoints // from the backend. for (let i = 0; i < 3; i++) { const lineNumber = i; // Push the breakpoint to our mock storage. The storage will be then used // to initialize the breakpoint manager. breakpoints.push({ url: URL, resourceTypeName: 'script', lineNumber, condition: '' as Breakpoints.BreakpointManager.UserCondition, enabled: true, isLogpoint: false, }); // When the mock backend receives a request for this breakpoint, it will // respond and record the request. Also, once we receive the void backend .responderToBreakpointByUrlRequest( URL, lineNumber)({breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: []}) .then(() => { breakpointRequestLinesReceived.add(lineNumber); if (breakpointRequestLinesReceived.size === expectedBreakpointLines.length) { resolve(Array.from(breakpointRequestLinesReceived).sort((l, r) => l - r)); } }, reject); } // Re-create the breakpoint manager and the target. const setting = Common.Settings.Settings.instance().createLocalSetting('breakpoints', breakpoints); setting.set(breakpoints); // Create the breakpoint manager, request placing on the two latest breakpoints in the backend. Breakpoints.BreakpointManager.BreakpointManager.instance({ forceNew: true, targetManager, workspace, debuggerWorkspaceBinding, restoreInitialBreakpointCount: expectedBreakpointLines.length, }); target = createTarget(); SDK.TargetManager.TargetManager.instance().setScopeTarget(target); }); assert.deepEqual(Array.from(await breakpointRequestLines), expectedBreakpointLines); }); describe('with instrumentation breakpoints turned on', () => { beforeEach(() => { const targetManager = SDK.TargetManager.TargetManager.instance(); const workspace = Workspace.Workspace.WorkspaceImpl.instance(); Root.Runtime.experiments.enableForTest(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS); breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); }); afterEach(() => { Root.Runtime.experiments.disableForTest(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS); }); async function testBreakpointMovedOnInstrumentationBreak( fileSystemPath: Platform.DevToolsPath.UrlString, fileSystemFileUrl: Platform.DevToolsPath.UrlString, content: string, type?: Persistence.PlatformFileSystem.PlatformFileSystemType) { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const {uiSourceCode: fileSystemUiSourceCode, project} = createFileSystemFileForPersistenceTests( {fileSystemFileUrl, fileSystemPath, type}, scriptDescription.url, content, target); const breakpointLine = 0; const resolvedBreakpointLine = 1; // Set the breakpoint on the file system uiSourceCode. await breakpointManager.setBreakpoint(fileSystemUiSourceCode, breakpointLine, 0, ...DEFAULT_BREAKPOINT); // Add the script. const script = await backend.addScript(target, scriptDescription, null); const uiSourceCode = debuggerWorkspaceBinding.uiSourceCodeForScript(script); assert.exists(uiSourceCode); assert.strictEqual(uiSourceCode.project().type(), Workspace.Workspace.projectTypes.Network); // Set the breakpoint response for our upcoming request. void backend.responderToBreakpointByUrlRequest(URL, breakpointLine)({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: script.scriptId, lineNumber: resolvedBreakpointLine, columnNumber: 0, }, ], }); // Register our interest in an outgoing 'resume', which should be sent as soon as // we have set up all breakpoints during the instrumentation pause. const resumeSentPromise = registerListenerOnOutgoingMessage('Debugger.resume'); // Inform the front-end about an instrumentation break. backend.dispatchDebuggerPause(script, Protocol.Debugger.PausedEventReason.Instrumentation); // Wait for the breakpoints to be set, and the resume to be sent. await resumeSentPromise; // Verify that the network uiSourceCode has the breakpoint that we originally set // on the file system uiSourceCode. const reloadedBoundLocations = breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode); assert.lengthOf(reloadedBoundLocations, 1); assert.strictEqual(resolvedBreakpointLine, reloadedBoundLocations[0].uiLocation.lineNumber); assert.strictEqual(0, reloadedBoundLocations[0].uiLocation.columnNumber); project.dispose(); } it('can restore breakpoints in scripts', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); const breakpointLine = 0; const resolvedBreakpointLine = 3; // Add script. const scriptInfo = {url: URL, content: 'console.log(\'hello\')'}; const script = await backend.addScript(target, scriptInfo, null); // Get the uiSourceCode for the source. const uiSourceCode = debuggerWorkspaceBinding.uiSourceCodeForScript(script); assert.exists(uiSourceCode); // Set the breakpoint on the front-end/model side. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, breakpointLine, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Set the breakpoint response for our upcoming request. void backend.responderToBreakpointByUrlRequest(URL, breakpointLine)({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: script.scriptId, lineNumber: resolvedBreakpointLine, columnNumber: 0, }, ], }); await breakpoint.refreshInDebugger(); assert.deepEqual(Array.from(breakpoint.getUiSourceCodes()), [uiSourceCode]); // Verify the restored position. const boundLocations = breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode); assert.lengthOf(boundLocations, 1); assert.strictEqual(resolvedBreakpointLine, boundLocations[0].uiLocation.lineNumber); assert.strictEqual(0, boundLocations[0].uiLocation.columnNumber); // Disconnect from the target. This will also unload the script. breakpointManager.targetManager.removeTarget(target); // Make sure the source code for the script was removed from the breakpoint. assert.strictEqual(breakpoint.getUiSourceCodes().size, 0); // Remove the breakpoint. await breakpoint.remove(true /* keepInStorage */); // Create a new target. target = createTarget(); SDK.TargetManager.TargetManager.instance().setScopeTarget(target); const reloadedDebuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(reloadedDebuggerModel); // Add the same script under a different scriptId. const reloadedScript = await backend.addScript(target, scriptInfo, null); // Get the uiSourceCode for the original source. const reloadedUiSourceCode = debuggerWorkspaceBinding.uiSourceCodeForScript(reloadedScript); assert.exists(reloadedUiSourceCode); // Set the breakpoint response for our upcoming request. void backend.responderToBreakpointByUrlRequest(URL, breakpointLine)({ breakpointId: 'RELOADED_BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: reloadedScript.scriptId, lineNumber: resolvedBreakpointLine, columnNumber: 0, }, ], }); // Register our interest in an outgoing 'resume', which should be sent as soon as // we have set up all breakpoints during the instrumentation pause. const resumeSentPromise = registerListenerOnOutgoingMessage('Debugger.resume'); // Inform the front-end about an instrumentation break. backend.dispatchDebuggerPause(reloadedScript, Protocol.Debugger.PausedEventReason.Instrumentation); // Wait for the breakpoints to be set, and the resume to be sent. await resumeSentPromise; // Verify the restored position. const reloadedBoundLocations = breakpointManager.breakpointLocationsForUISourceCode(reloadedUiSourceCode); assert.lengthOf(reloadedBoundLocations, 1); assert.strictEqual(resolvedBreakpointLine, reloadedBoundLocations[0].uiLocation.lineNumber); assert.strictEqual(0, reloadedBoundLocations[0].uiLocation.columnNumber); }); it('can restore breakpoints in a default-mapped inline scripts without sourceURL comment', async () => { const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(debuggerModel); // Add script. const script = await backend.addScript(target, inlineScriptDescription, null); // Get the uiSourceCode for the source. This is the uiSourceCode in the DefaultScriptMapping, // as we haven't registered the uiSourceCode for the html file. const uiSourceCode = await debuggerWorkspaceBinding.uiSourceCodeForScript(script); assert.exists(uiSourceCode); assert.strictEqual(uiSourceCode.project().type(), Workspace.Workspace.projectTypes.Debugger); // Set the breakpoint on the front-end/model side. The line number is relative to the v8 script. const breakpoint = await breakpointManager.setBreakpoint(uiSourceCode, BREAKPOINT_SCRIPT_LINE, 0, ...DEFAULT_BREAKPOINT); assert.exists(breakpoint); // Set the breakpoint response for our upcoming request. void backend.responderToBreakpointByUrlRequest(URL_HTML, INLINE_BREAKPOINT_RAW_LINE)({ breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: script.scriptId, lineNumber: INLINE_BREAKPOINT_RAW_LINE, columnNumber: 0, }, ], }); await breakpoint.refreshInDebugger(); assert.deepEqual(Array.from(breakpoint.getUiSourceCodes()), [uiSourceCode]); // Verify the position. const boundLocations = breakpointManager.breakpointLocationsForUISourceCode(uiSourceCode); assert.lengthOf(boundLocations, 1); assert.strictEqual(BREAKPOINT_SCRIPT_LINE, boundLocations[0].uiLocation.lineNumber); assert.strictEqual(0, boundLocations[0].uiLocation.columnNumber); // Disconnect from the target. This will also unload the script. breakpointManager.targetManager.removeTarget(target); // Make sure the source code for the script was removed from the breakpoint. assert.strictEqual(breakpoint.getUiSourceCodes().size, 0); // Remove the breakpoint. await breakpoint.remove(true /* keepInStorage */); // Create a new target. target = createTarget(); SDK.TargetManager.TargetManager.instance().setScopeTarget(target); const reloadedDebuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.exists(reloadedDebuggerModel); // Add the same script under a different scriptId. const reloadedScript = await backend.addScript(target, inlineScriptDescription, null); // Get the uiSourceCode for the source. This is the uiSourceCode in the DefaultScriptMapping, // as we haven't registered the uiSourceCode for the html file. const reloadedUiSourceCode = debuggerWorkspaceBinding.uiSourceCodeForScript(reloadedScript); assert.exists(reloadedUiSourceCode); assert.strictEqual(reloadedUiSourceCode.project().type(), Workspace.Workspace.projectTypes.Debugger); // Set the breakpoint response for our upcoming request. void backend.responderToBreakpointByUrlRequest(URL_HTML, INLINE_BREAKPOINT_RAW_LINE)({ breakpointId: 'RELOADED_BREAK_ID' as Protocol.Debugger.BreakpointId, locations: [ { scriptId: reloadedScript.scriptId, lineNumber: INLINE_BREAKPOINT_RAW_LINE, columnNumber: 0, }, ], }); // Register our interest in an outgoing 'resume', which should be sent as soon as // we have set up all breakpoints during the instrumentation pause. const resumeSentPromise = registerListenerOnOutgoingMessage('Debugger.resume'); // Inform the front-end about an instrumentation break. backend.dispatchDebuggerPause(reloadedScript, Protocol.Debugger.PausedEventReason.Instrumentation); // Wait for the breakpoints to be set, and