chrome-devtools-frontend
Version:
Chrome DevTools UI
1,108 lines (931 loc) • 85.9 kB
text/typescript
// 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