chrome-devtools-frontend
Version:
Chrome DevTools UI
396 lines (348 loc) • 15.4 kB
text/typescript
// Copyright 2021 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 Platform from '../../../../core/platform/platform.js';
import * as SDK from '../../../../core/sdk/sdk.js';
import type * as Protocol from '../../../../generated/protocol.js';
import * as Bindings from '../../../../models/bindings/bindings.js';
import * as Breakpoints from '../../../../models/breakpoints/breakpoints.js';
import * as Workspace from '../../../../models/workspace/workspace.js';
import {findMenuItemWithLabel} from '../../../../testing/ContextMenuHelpers.js';
import {
createTarget,
describeWithEnvironment,
} from '../../../../testing/EnvironmentHelpers.js';
import {
describeWithMockConnection,
dispatchEvent,
} from '../../../../testing/MockConnection.js';
import {MockProtocolBackend} from '../../../../testing/MockScopeChain.js';
import * as UI from '../../legacy.js';
import * as Components from './utils.js';
const {urlString} = Platform.DevToolsPath;
const scriptId1 = '1' as Protocol.Runtime.ScriptId;
const scriptId2 = '2' as Protocol.Runtime.ScriptId;
const executionContextId = 1234 as Protocol.Runtime.ExecutionContextId;
const simpleScriptContent = `
function foo(x) {
const y = x + 3;
return y;
}
`;
describeWithMockConnection('Linkifier', () => {
function setUpEnvironment() {
const target = createTarget();
const linkifier = new Components.Linkifier.Linkifier(100, false);
linkifier.targetAdded(target);
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
const forceNew = true;
const targetManager = target.targetManager();
const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew,
resourceMapping,
targetManager,
});
Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew, debuggerWorkspaceBinding});
Breakpoints.BreakpointManager.BreakpointManager.instance(
{forceNew, targetManager, workspace, debuggerWorkspaceBinding});
const backend = new MockProtocolBackend();
return {target, linkifier, backend};
}
describe('Linkifier.linkifyURL', () => {
it('prefers text over the URL if it is present', async () => {
const url = urlString`http://www.example.com`;
const link = Components.Linkifier.Linkifier.linkifyURL(url, {
text: 'foo',
showColumnNumber: false,
inlineFrameIndex: 1,
});
assert.strictEqual(link.innerText, 'foo');
});
it('falls back to the URL if given an empty text value', async () => {
const url = urlString`http://www.example.com`;
const link = Components.Linkifier.Linkifier.linkifyURL(url, {
text: '',
showColumnNumber: false,
inlineFrameIndex: 1,
});
assert.strictEqual(link.innerText, 'www.example.com');
});
it('falls back to unknown if the URL and text are empty', async () => {
const url = urlString``;
const link = Components.Linkifier.Linkifier.linkifyURL(url, {
text: '',
showColumnNumber: false,
inlineFrameIndex: 1,
});
assert.strictEqual(link.innerText, '(unknown)');
});
});
it('creates an empty placeholder anchor if the debugger is disabled and no url exists', () => {
const {target, linkifier} = setUpEnvironment();
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
assert.exists(debuggerModel);
void debuggerModel.suspendModel();
const lineNumber = 4;
const url = Platform.DevToolsPath.EmptyUrlString;
const anchor = linkifier.maybeLinkifyScriptLocation(target, scriptId1, url, lineNumber);
assert.exists(anchor);
assert.strictEqual(anchor.textContent, '');
const info = Components.Linkifier.Linkifier.linkInfo(anchor);
assert.exists(info);
assert.isNull(info.uiLocation);
});
it('resolves url and updates link as soon as debugger is enabled', done => {
const {target, linkifier} = setUpEnvironment();
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
assert.exists(debuggerModel);
void debuggerModel.suspendModel();
const lineNumber = 4;
// Explicitly set url to empty string and let it resolve through the live location.
const url = Platform.DevToolsPath.EmptyUrlString;
const anchor = linkifier.maybeLinkifyScriptLocation(target, scriptId1, url, lineNumber);
assert.exists(anchor);
assert.strictEqual(anchor.textContent, '');
void debuggerModel.resumeModel();
const scriptParsedEvent: Protocol.Debugger.ScriptParsedEvent = {
scriptId: scriptId1,
url: 'https://www.google.com/script.js',
startLine: 0,
startColumn: 0,
endLine: 10,
endColumn: 10,
executionContextId,
hash: '',
buildId: '',
isLiveEdit: false,
sourceMapURL: undefined,
hasSourceURL: false,
length: 10,
};
dispatchEvent(target, 'Debugger.scriptParsed', scriptParsedEvent);
const callback: MutationCallback = function(mutations: MutationRecord[]) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const info = Components.Linkifier.Linkifier.linkInfo(anchor);
assert.exists(info);
assert.exists(info.uiLocation);
assert.strictEqual(anchor.textContent, `script.js:${lineNumber + 1}`);
observer.disconnect();
done();
}
}
};
const observer = new MutationObserver(callback);
observer.observe(anchor, {childList: true});
});
it('always favors script ID over url', done => {
const {target, linkifier} = setUpEnvironment();
const lineNumber = 4;
const url = 'https://www.google.com/script.js';
const scriptParsedEvent1: Protocol.Debugger.ScriptParsedEvent = {
scriptId: scriptId1,
url,
startLine: 0,
startColumn: 0,
endLine: 10,
endColumn: 10,
executionContextId,
hash: '',
buildId: '',
isLiveEdit: false,
sourceMapURL: undefined,
hasSourceURL: false,
length: 10,
};
dispatchEvent(target, 'Debugger.scriptParsed', scriptParsedEvent1);
// Ask for a link to a script that has not been registered yet, but has the same url.
const anchor = linkifier.maybeLinkifyScriptLocation(target, scriptId2, urlString`${url}`, lineNumber);
assert.exists(anchor);
// This link should not pick up the first script with the same url, since there's no
// warranty that the first script has anything to do with this one (other than having
// the same url).
const info = Components.Linkifier.Linkifier.linkInfo(anchor);
assert.exists(info);
assert.isNull(info.uiLocation);
const scriptParsedEvent2: Protocol.Debugger.ScriptParsedEvent = {
scriptId: scriptId2,
url,
startLine: 0,
startColumn: 0,
endLine: 10,
endColumn: 10,
executionContextId,
hash: '',
buildId: '',
isLiveEdit: false,
sourceMapURL: undefined,
hasSourceURL: false,
length: 10,
};
dispatchEvent(target, 'Debugger.scriptParsed', scriptParsedEvent2);
const callback: MutationCallback = function(mutations: MutationRecord[]) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const info = Components.Linkifier.Linkifier.linkInfo(anchor);
assert.exists(info);
assert.exists(info.uiLocation);
// Make sure that a uiSourceCode is linked to that anchor.
assert.exists(info.uiLocation.uiSourceCode);
observer.disconnect();
done();
}
}
};
const observer = new MutationObserver(callback);
observer.observe(anchor, {childList: true});
});
it('optionally shows column numbers in the link text', done => {
const {target, linkifier} = setUpEnvironment();
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
assert.exists(debuggerModel);
void debuggerModel.suspendModel();
const lineNumber = 4;
const options = {columnNumber: 8, showColumnNumber: true, inlineFrameIndex: 0};
// Explicitly set url to empty string and let it resolve through the live location.
const url = Platform.DevToolsPath.EmptyUrlString;
const anchor = linkifier.maybeLinkifyScriptLocation(target, scriptId1, url, lineNumber, options);
assert.exists(anchor);
assert.strictEqual(anchor.textContent, '');
void debuggerModel.resumeModel();
const scriptParsedEvent: Protocol.Debugger.ScriptParsedEvent = {
scriptId: scriptId1,
url: 'https://www.google.com/script.js',
startLine: 0,
startColumn: 0,
endLine: 10,
endColumn: 10,
executionContextId,
hash: '',
buildId: '',
isLiveEdit: false,
sourceMapURL: undefined,
hasSourceURL: false,
length: 10,
};
dispatchEvent(target, 'Debugger.scriptParsed', scriptParsedEvent);
const callback: MutationCallback = function(mutations: MutationRecord[]) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const info = Components.Linkifier.Linkifier.linkInfo(anchor);
assert.exists(info);
assert.exists(info.uiLocation);
assert.strictEqual(anchor.textContent, `script.js:${lineNumber + 1}:${options.columnNumber + 1}`);
observer.disconnect();
done();
}
}
};
const observer = new MutationObserver(callback);
observer.observe(anchor, {childList: true});
});
it('linkifyStackTraceTopFrame without a target returns an initiator link', () => {
const lineNumber = 165;
const {linkifier} = setUpEnvironment();
const anchor = linkifier.linkifyStackTraceTopFrame(null, {
callFrames: [{
url: 'https://w.com/a.js',
functionName: 'wow',
scriptId: '1' as Protocol.Runtime.ScriptId,
lineNumber,
columnNumber: 15,
}],
});
assert.exists(anchor);
assert.strictEqual(anchor.textContent, `w.com/a.js:${lineNumber + 1}`);
});
describe('maybeLinkifyScriptLocation', () => {
it('uses the BreakLocation as a revealable if the option is provided and a breakpoint is at the given location',
async () => {
const {target, linkifier, backend} = setUpEnvironment();
const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance();
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
const lineNumber = 1;
const columnNumber = 0;
const url = urlString`https://www.google.com/script.js`;
const script = await backend.addScript(target, {content: simpleScriptContent, url}, null);
const uiSourceCode = debuggerWorkspaceBinding.uiSourceCodeForScript(script);
assert.exists(uiSourceCode);
const responder = backend.responderToBreakpointByUrlRequest(url, lineNumber);
void responder({
breakpointId: 'BREAK_ID' as Protocol.Debugger.BreakpointId,
locations: [
{
scriptId: script.scriptId,
lineNumber,
columnNumber,
},
],
});
const breakpoint = await breakpointManager.setBreakpoint(
uiSourceCode, lineNumber, columnNumber, 'x' as Breakpoints.BreakpointManager.UserCondition,
/* enabled */ true, /* isLogpoint */ true, Breakpoints.BreakpointManager.BreakpointOrigin.USER_ACTION);
assert.exists(breakpoint);
// Create a link that matches exactly the breakpoint location.
const anchor = linkifier.maybeLinkifyScriptLocation(
target, script.scriptId, url, lineNumber, {inlineFrameIndex: 0, revealBreakpoint: true});
assert.exists(anchor);
await debuggerWorkspaceBinding.pendingLiveLocationChangesPromise();
// Assert that the linkinfo has the `BreakLocation` as its revealable.
// When clicking the link, `revealables` have predecence over e.g. the
// UILocation or url.
const linkInfo = Components.Linkifier.Linkifier.linkInfo(anchor);
assert.exists(linkInfo);
assert.propertyVal(linkInfo.revealable, 'breakpoint', breakpoint);
});
it('fires the LiveLocationUpdate event for each LiveLocation update', async () => {
const {target, linkifier, backend} = setUpEnvironment();
const eventCallback = sinon.stub();
linkifier.addEventListener(Components.Linkifier.Events.LIVE_LOCATION_UPDATED, eventCallback);
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
const lineNumber = 1;
const url = urlString`https://www.google.com/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 script = await backend.addScript(target, {content: 'function adder(n,r){const t=n+r;return t}', url}, {
url: 'https://www.google.com/script.js.map',
content: sourceMapContent,
});
linkifier.maybeLinkifyScriptLocation(target, script.scriptId, url, lineNumber);
await debuggerWorkspaceBinding.pendingLiveLocationChangesPromise();
sinon.assert.calledOnce(eventCallback);
// Detach the source map and check we get the update event.
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
assert.exists(debuggerModel);
debuggerModel.sourceMapManager().detachSourceMap(script);
await debuggerWorkspaceBinding.pendingLiveLocationChangesPromise();
// We currently receive more than one event after detaching the source map.
// This is also valid but might constitute unnecessary work.
assert.isTrue(eventCallback.callCount >= 2);
});
});
});
describeWithEnvironment('ContentProviderContextMenuProvider', () => {
it('does not add \'Open in new tab\'-entry for file URLs', async () => {
const provider = new Components.Linkifier.ContentProviderContextMenuProvider();
let contextMenu = new UI.ContextMenu.ContextMenu({} as Event);
let uiSourceCode = {
contentURL: () => 'https://www.example.com/index.html',
} as Workspace.UISourceCode.UISourceCode;
provider.appendApplicableItems({} as Event, contextMenu, uiSourceCode);
let openInNewTabItem = findMenuItemWithLabel(contextMenu.revealSection(), 'Open in new tab');
assert.exists(openInNewTabItem);
contextMenu = new UI.ContextMenu.ContextMenu({} as Event);
uiSourceCode = {
contentURL: () => 'file://usr/local/example/index.html',
} as Workspace.UISourceCode.UISourceCode;
provider.appendApplicableItems({} as Event, contextMenu, uiSourceCode);
openInNewTabItem = findMenuItemWithLabel(contextMenu.revealSection(), 'Open in new tab');
assert.isUndefined(openInNewTabItem);
});
});