blob: 70e0a12c21a9f1f72ab95b00296c03eb92881778 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// 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 {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js';
import {createResource, getMainFrame} from '../../testing/ResourceTreeHelpers.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';
import * as Bindings from './bindings.js';
const {urlString} = Platform.DevToolsPath;
describeWithMockConnection('ResourceMapping', () => {
let debuggerModel: SDK.DebuggerModel.DebuggerModel;
let resourceMapping: Bindings.ResourceMapping.ResourceMapping;
let uiSourceCode: Workspace.UISourceCode.UISourceCode;
let workspace: Workspace.Workspace.WorkspaceImpl;
let target: SDK.Target.Target;
// This test simulates the behavior of the ResourceMapping with the
// following document, which contains two inline <script>s, one with
// a `//# sourceURL` annotation and one without.
//
// <!DOCTYPE html>
// <html>
// <head>
// <meta charset=utf-8>
// <script>
// function foo() { console.log("foo"); }
// foo();
// //# sourceURL=webpack:///src/foo.js
// </script>
// </head>
// <body>
// <script>console.log("bar");</script>
// </body>
// </html>
//
const url = urlString`http://example.com/index.html`;
const SCRIPTS = [
{
scriptId: '1' as Protocol.Runtime.ScriptId,
startLine: 4,
startColumn: 8,
endLine: 8,
endColumn: 0,
sourceURL: urlString`webpack:///src/foo.js`,
hasSourceURLComment: true,
source: `\nfunction foo() { console.log("foo"); }
foo();
//# sourceURL=webpack:///src/foo.js`,
},
{
scriptId: '2' as Protocol.Runtime.ScriptId,
startLine: 11,
startColumn: 8,
endLine: 11,
endColumn: 27,
sourceURL: url,
hasSourceURLComment: false,
source: 'console.log("bar");',
},
];
const OTHER_SCRIPT_ID = '3' as Protocol.Runtime.ScriptId;
beforeEach(async () => {
target = createTarget();
const targetManager = target.targetManager();
targetManager.setScopeTarget(target);
workspace = Workspace.Workspace.WorkspaceImpl.instance();
resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance({forceNew: true, resourceMapping, targetManager});
const ignoreListManager = Workspace.IgnoreListManager.IgnoreListManager.instance({forceNew: true});
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew: true,
resourceMapping,
targetManager,
ignoreListManager,
workspace,
});
// Inject the HTML document resource.
createResource(getMainFrame(target), url, 'text/html', '');
uiSourceCode = workspace.uiSourceCodeForURL(url) as Workspace.UISourceCode.UISourceCode;
assert.isNotNull(uiSourceCode);
// Register the inline <script>s.
const hash = '';
const length = 0;
const embedderName = url;
const executionContextId = 1;
debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel;
SCRIPTS.forEach(({scriptId, startLine, startColumn, endLine, endColumn, sourceURL, hasSourceURLComment}) => {
debuggerModel.parsedScriptSource(
scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, undefined, false,
undefined, hasSourceURLComment, false, length, false, null, null, null, null, embedderName, null);
});
assert.lengthOf(debuggerModel.scripts(), SCRIPTS.length);
setMockConnectionResponseHandler('Debugger.getScriptSource', param => {
return {
scriptSource: SCRIPTS.find(s => s.scriptId === param.scriptId)?.source ?? '',
getError() {
return undefined;
},
};
});
});
it('creates UISourceCode for added target', () => {
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel)!;
resourceMapping.modelRemoved(resourceTreeModel);
assert.isNull(workspace.uiSourceCodeForURL(url));
resourceMapping.modelAdded(resourceTreeModel);
assert.isNotNull(workspace.uiSourceCodeForURL(url));
});
it('creates UISourceCode for added out of scope target', () => {
SDK.TargetManager.TargetManager.instance().setScopeTarget(null);
const otherUrl = urlString`http://example.com/other.html`;
createResource(getMainFrame(target), otherUrl, 'text/html', '');
uiSourceCode = workspace.uiSourceCodeForURL(otherUrl) as Workspace.UISourceCode.UISourceCode;
assert.isNotNull(uiSourceCode);
});
describe('uiLocationToJSLocations', () => {
it('does not map locations outside of <script> tags', () => {
assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, 0, 0));
SCRIPTS.forEach(({startLine, startColumn, endLine, endColumn}) => {
assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, startColumn - 1));
assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, endLine, endColumn));
});
assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, 12, 1));
});
it('correctly maps inline <script> with a //# sourceURL annotation', () => {
const {scriptId, startLine, startColumn, endLine, endColumn} = SCRIPTS[0];
// Debugger locations in scripts with sourceURL annotations are relative to the beginning
// of the script, rather than relative to the start of the surrounding document.
assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, startColumn), [
debuggerModel.createRawLocationByScriptId(scriptId, 0, 0),
]);
assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, startColumn + 3), [
// This location does not actually exist in the simulated document, but
// the ResourceMapping doesn't know (and shouldn't care) about that.
debuggerModel.createRawLocationByScriptId(scriptId, 0, 3),
]);
assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine + 1, 5), [
debuggerModel.createRawLocationByScriptId(scriptId, 1, 5),
]);
assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, endLine - 1, endColumn), [
debuggerModel.createRawLocationByScriptId(scriptId, endLine - startLine - 1, endColumn),
]);
});
it('correctly maps inline <script> without //# sourceURL annotation', () => {
const {scriptId, startLine, startColumn, endLine, endColumn} = SCRIPTS[1];
// Debugger locations in scripts without sourceURL annotations are relative to the
// beginning of the surrounding document, so this is basically a 1-1 mapping.
assert.strictEqual(endLine, startLine);
for (let column = startColumn; column < endColumn; ++column) {
assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, column), [
debuggerModel.createRawLocationByScriptId(scriptId, startLine, column),
]);
}
});
});
describe('uiLocationRangeToRSLocationRanges', () => {
it('correctly reports all inline <script>s when querying the whole document', () => {
const rawLocationRanges = resourceMapping.uiLocationRangeToJSLocationRanges(
uiSourceCode, new TextUtils.TextRange.TextRange(0, 0, 14, 0));
assert.exists(rawLocationRanges);
assert.lengthOf(rawLocationRanges, SCRIPTS.length);
for (let i = 0; i < SCRIPTS.length; ++i) {
let {startLine, startColumn, endLine, endColumn} = SCRIPTS[i];
const {scriptId, hasSourceURLComment} = SCRIPTS[i];
const {start, end} = rawLocationRanges[i];
assert.strictEqual(start.scriptId, scriptId);
assert.strictEqual(end.scriptId, scriptId);
if (hasSourceURLComment) {
if (endLine === startLine) {
endColumn -= startColumn;
}
endLine -= startLine;
startLine = 0;
startColumn = 0;
}
assert.strictEqual(start.lineNumber, startLine);
assert.strictEqual(start.columnNumber, startColumn);
assert.strictEqual(end.lineNumber, endLine);
assert.strictEqual(end.columnNumber, endColumn);
}
});
});
describe('jsLocationToUILocation', () => {
it('does not map locations of unrelated scripts', () => {
assert.isNull(
resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(OTHER_SCRIPT_ID, 1, 1)));
SCRIPTS.forEach(({startLine, startColumn, endLine, endColumn}) => {
// Check that we also don't reverse map locations that overlap with the existing script locations.
assert.isNull(resourceMapping.jsLocationToUILocation(
debuggerModel.createRawLocationByScriptId(OTHER_SCRIPT_ID, startLine, startColumn)));
assert.isNull(resourceMapping.jsLocationToUILocation(
debuggerModel.createRawLocationByScriptId(OTHER_SCRIPT_ID, endLine, endColumn)));
});
});
it('correctly maps inline <script> with //# sourceURL annotation', () => {
const {scriptId, startLine, startColumn, endLine, endColumn} = SCRIPTS[0];
// Debugger locations in scripts with sourceURL annotations are relative to the beginning
// of the script, rather than relative to the start of the surrounding document.
assert.deepEqual(
resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, 0, 0)),
new Workspace.UISourceCode.UILocation(uiSourceCode, startLine, startColumn));
assert.deepEqual(
resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, 0, 55)),
// This location does not actually exist in the simulated document, but
// the ResourceMapping doesn't know (and shouldn't care) about that.
new Workspace.UISourceCode.UILocation(uiSourceCode, startLine, startColumn + 55));
assert.deepEqual(
resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, 2, 0)),
new Workspace.UISourceCode.UILocation(uiSourceCode, startLine + 2, 0));
assert.deepEqual(
resourceMapping.jsLocationToUILocation(
debuggerModel.createRawLocationByScriptId(scriptId, endLine - startLine, endColumn)),
new Workspace.UISourceCode.UILocation(uiSourceCode, endLine, endColumn));
});
it('correctly maps inline <script> without //# sourceURL annotation', () => {
const {scriptId, startLine, startColumn, endLine, endColumn} = SCRIPTS[1];
// Debugger locations in scripts without sourceURL annotations are relative to the
// beginning of the surrounding document, so this is basically a 1-1 mapping.
assert.strictEqual(endLine, startLine);
for (let column = startColumn; column < endColumn; ++column) {
assert.deepEqual(
resourceMapping.jsLocationToUILocation(
debuggerModel.createRawLocationByScriptId(scriptId, startLine, column)),
new Workspace.UISourceCode.UILocation(uiSourceCode, startLine, column));
}
});
});
describe('getMappedLines', () => {
it('reports line numbers for all inline scripts', () => {
const expectedLines = new Set();
SCRIPTS.forEach(({startLine, endLine}) => {
for (let line = startLine; line <= endLine; ++line) {
expectedLines.add(line);
}
});
const mappedLines = resourceMapping.getMappedLines(uiSourceCode);
assert.deepEqual(mappedLines, expectedLines);
});
});
describe('functionBoundsAtRawLocation', () => {
function makeLocation(script: typeof SCRIPTS[number], line: number, column: number): SDK.DebuggerModel.Location {
return new SDK.DebuggerModel.Location(debuggerModel, script.scriptId, line, column);
}
it('finds the function bounds for an inline script', async () => {
const functionBounds = await resourceMapping.functionBoundsAtRawLocation(makeLocation(SCRIPTS[0], 5, 16));
assert.isOk(functionBounds);
// TODO(crbug.com/452333154)
// assert.strictEqual(functionBounds.name, 'foo');
assert.strictEqual(functionBounds.range.startLine, 5);
assert.strictEqual(functionBounds.range.startColumn, 12);
assert.strictEqual(functionBounds.range.endLine, 5);
assert.strictEqual(functionBounds.range.endColumn, 38);
});
it('finds no function bounds for an inline script with no functions', async () => {
const functionBounds = await resourceMapping.functionBoundsAtRawLocation(makeLocation(SCRIPTS[1], 11, 9));
assert.isNotOk(functionBounds);
});
});
});