| // 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); |
| }); |
| }); |
| }); |