| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as Protocol from '../../generated/protocol.js'; |
| import {createTarget} from '../../testing/EnvironmentHelpers.js'; |
| import {describeWithMockConnection} from '../../testing/MockConnection.js'; |
| import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js'; |
| import {setupSettingsHooks} from '../../testing/SettingsHelpers.js'; |
| import {setupPageResourceLoaderForSourceMap} from '../../testing/SourceMapHelpers.js'; |
| import * as Platform from '../platform/platform.js'; |
| |
| import * as SDK from './sdk.js'; |
| |
| const {urlString} = Platform.DevToolsPath; |
| const content = JSON.stringify({ |
| version: 3, |
| file: '/script.js', |
| mappings: '', |
| sources: [ |
| '/original-script.js', |
| ], |
| }); |
| |
| describeWithMockConnection('SourceMapManager', () => { |
| it('uses url for a worker\'s source maps from frame', async () => { |
| setupPageResourceLoaderForSourceMap(content); |
| const frameUrl = urlString`https://frame-host/index.html`; |
| const scriptUrl = urlString`https://script-host/script.js`; |
| const sourceUrl = urlString`script.js`; |
| const sourceMapUrl = urlString`script.js.map`; |
| |
| const mainTarget = |
| createTarget({id: 'main' as Protocol.Target.TargetID, name: 'main', type: SDK.Target.Type.FRAME}); |
| mainTarget.setInspectedURL(frameUrl); |
| |
| const workerTarget = createTarget({ |
| id: 'worker' as Protocol.Target.TargetID, |
| name: 'worker', |
| type: SDK.Target.Type.Worker, |
| parentTarget: mainTarget, |
| }); |
| |
| const debuggerModel = workerTarget.model(SDK.DebuggerModel.DebuggerModel); |
| assert.isNotNull(debuggerModel); |
| const sourceMapManager = debuggerModel.sourceMapManager(); |
| |
| const script = new SDK.Script.Script( |
| debuggerModel, '1' as Protocol.Runtime.ScriptId, scriptUrl, 0, 0, 0, 0, 0, '', false, false, sourceMapUrl, |
| false, 0, null, null, null, null, null, null, null); |
| |
| sourceMapManager.attachSourceMap(script, sourceUrl, sourceMapUrl); |
| |
| const sourceMap = await sourceMapManager.sourceMapForClientPromise(script); |
| // Check that the URLs are resolved relative to the frame. |
| assert.strictEqual(sourceMap?.url(), urlString`https://frame-host/script.js.map`); |
| assert.deepEqual(sourceMap?.sourceURLs(), [urlString`https://frame-host/original-script.js`]); |
| }); |
| |
| it('can handle source maps in a data URL frame', async () => { |
| setupPageResourceLoaderForSourceMap(content); |
| const sourceUrl = urlString`script.js`; |
| const sourceMapUrl = urlString`${`data:test/html;base64,${btoa(content)}`}`; |
| const frameSource = |
| '<script>0\n//# sourceURL=' + sourceUrl + '\n//# sourceMappingURL=' + sourceMapUrl + '</script>'; |
| const frameUrl = urlString`${`data:test/html;base64,${btoa(frameSource)}`}`; |
| const scriptUrl = urlString`https://script-host/script.js`; |
| |
| const mainTarget = |
| createTarget({id: 'main' as Protocol.Target.TargetID, name: 'main', type: SDK.Target.Type.FRAME}); |
| mainTarget.setInspectedURL(frameUrl); |
| |
| const debuggerModel = mainTarget.model(SDK.DebuggerModel.DebuggerModel); |
| assert.isNotNull(debuggerModel); |
| const sourceMapManager = debuggerModel.sourceMapManager(); |
| |
| const script = new SDK.Script.Script( |
| debuggerModel, '1' as Protocol.Runtime.ScriptId, scriptUrl, 0, 0, 0, 0, 0, '', false, false, sourceMapUrl, |
| false, 0, null, null, null, null, null, null, null); |
| |
| sourceMapManager.attachSourceMap(script, sourceUrl, sourceMapUrl); |
| |
| const sourceMap = await sourceMapManager.sourceMapForClientPromise(script); |
| assert.deepEqual(sourceMap?.sourceURLs(), [urlString`/original-script.js`]); |
| }); |
| }); |
| |
| describe('SourceMapManager', () => { |
| const sourceURL = urlString`http://localhost/foo.js`; |
| const sourceMappingURL = `${sourceURL}.map`; |
| |
| setupRuntimeHooks(); |
| setupSettingsHooks(); |
| |
| beforeEach(() => { |
| SDK.TargetManager.TargetManager.instance({forceNew: true}); |
| SDK.PageResourceLoader.PageResourceLoader.instance({forceNew: true, loadOverride: null, maxConcurrentLoads: 1}); |
| }); |
| |
| afterEach(() => { |
| SDK.PageResourceLoader.PageResourceLoader.removeInstance(); |
| SDK.TargetManager.TargetManager.removeInstance(); |
| }); |
| |
| class MockClient implements SDK.FrameAssociated.FrameAssociated { |
| constructor(private target: SDK.Target.Target) { |
| } |
| |
| createPageResourceLoadInitiator(): SDK.PageResourceLoader.PageResourceLoadInitiator { |
| return {target: this.target, frameId: null, initiatorUrl: null}; |
| } |
| |
| debugId(): SDK.SourceMap.DebugId|null { |
| return null; |
| } |
| } |
| |
| describe('attachSourceMap', () => { |
| it('catches attempts to attach twice for the same client', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| assert.throws(() => sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL)); |
| await sourceMapManager.sourceMapForClientPromise(client); |
| }); |
| |
| it('triggers the correct lifecycle events when loading succeeds', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| const sourceMapWillAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, sourceMapWillAttach); |
| const sourceMapAttached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, sourceMapAttached); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| assert.strictEqual(sourceMapWillAttach.callCount, 1, 'SourceMapWillAttach events'); |
| sinon.assert.calledWith(sourceMapWillAttach, sinon.match.hasNested('data.client', client)); |
| const sourceMap = await sourceMapManager.sourceMapForClientPromise(client); |
| assert.strictEqual(sourceMapAttached.callCount, 1, 'SourceMapAttached events'); |
| sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.client', client)); |
| sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.sourceMap', sourceMap)); |
| assert.isTrue(sourceMapAttached.calledAfter(sourceMapWillAttach)); |
| }); |
| |
| it('triggers the correct lifecycle events when loading fails', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| const sourceMapWillAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, sourceMapWillAttach); |
| const sourceMapFailedToAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').rejects('Error'); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| assert.strictEqual(sourceMapWillAttach.callCount, 1, 'SourceMapWillAttach events'); |
| sinon.assert.calledWith(sourceMapWillAttach, sinon.match.hasNested('data.client', client)); |
| await sourceMapManager.sourceMapForClientPromise(client); |
| assert.strictEqual(sourceMapFailedToAttach.callCount, 1, 'SourceMapFailedToAttach events'); |
| sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client)); |
| assert.isTrue(sourceMapFailedToAttach.calledAfter(sourceMapWillAttach)); |
| }); |
| |
| it('correctly handles the case where sourcemap reattaches immediately', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| const sourceMapAttached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, sourceMapAttached); |
| const sourceMapFailedToAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| sourceMapManager.detachSourceMap(client); |
| sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client)); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| await sourceMapManager.sourceMapForClientPromise(client); |
| assert.strictEqual(sourceMapAttached.callCount, 1, 'SourceMapAttached events'); |
| sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.client', client)); |
| assert.isTrue(sourceMapAttached.calledAfter(sourceMapFailedToAttach)); |
| }); |
| |
| it('correctly handles separate clients with same sourceURL and sourceMappingURL', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client1 = new MockClient(target); |
| const client2 = new MockClient(target); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client1, sourceURL, sourceMappingURL); |
| sourceMapManager.attachSourceMap(client2, sourceURL, sourceMappingURL); |
| const [sourceMap1, sourceMap2] = await Promise.all([ |
| sourceMapManager.sourceMapForClientPromise(client1), |
| sourceMapManager.sourceMapForClientPromise(client2), |
| ]); |
| assert.notStrictEqual(sourceMap1, sourceMap2); |
| }); |
| |
| it('defers loading sourcemaps while disabled', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| sourceMapManager.setEnabled(false); |
| const client = new MockClient(target); |
| const loadResource = sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource'); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| assert.strictEqual(loadResource.callCount, 0, 'loadResource calls'); |
| assert.isUndefined(sourceMapManager.sourceMapForClient(client)); |
| assert.isUndefined(await sourceMapManager.sourceMapForClientPromise(client)); |
| sourceMapManager.setEnabled(true); |
| assert.strictEqual(loadResource.callCount, 1, 'loadResource calls'); |
| await sourceMapManager.sourceMapForClientPromise(client); |
| }); |
| |
| it('does not attempt to load when attach is cancelled', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sourceMapManager.addEventListener( |
| SDK.SourceMapManager.Events.SourceMapWillAttach, |
| ({data: {client}}) => sourceMapManager.cancelAttachSourceMap(client)); |
| const sourceMapFailedToAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach); |
| const loadResource = sinon.spy(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource'); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| assert.strictEqual(loadResource.callCount, 0, 'loadResource calls'); |
| await sourceMapManager.sourceMapForClientPromise(client); |
| assert.strictEqual(sourceMapFailedToAttach.callCount, 1, 'SourceMapFailedToAttach events'); |
| sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client)); |
| }); |
| }); |
| |
| describe('detachSourceMap', () => { |
| it('silently ignores unknown clients', () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sourceMapManager.detachSourceMap(client); |
| }); |
| |
| it('triggers the correct lifecycle events', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| const sourceMapDetached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| const sourceMap = await sourceMapManager.sourceMapForClientPromise(client); |
| sourceMapManager.detachSourceMap(client); |
| assert.strictEqual(sourceMapDetached.callCount, 1, 'SourceMapDetached events'); |
| sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.client', client)); |
| sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.sourceMap', sourceMap)); |
| }); |
| |
| it('triggers the correct lifecycle events when disabled', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sourceMapManager.setEnabled(false); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| const sourceMapFailedToAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach); |
| const sourceMapDetached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached); |
| |
| sourceMapManager.detachSourceMap(client); |
| |
| assert.strictEqual(sourceMapFailedToAttach.callCount, 0, 'SourceMapFailedToAttach events'); |
| assert.strictEqual(sourceMapDetached.callCount, 0, 'SourceMapDetached events'); |
| }); |
| }); |
| |
| describe('setEnabled', () => { |
| it('triggers the correct lifecycle events when disabling while attaching', () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').returns(new Promise(() => {})); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| const sourceMapFailedToAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach); |
| |
| sourceMapManager.setEnabled(false); |
| |
| assert.strictEqual(sourceMapFailedToAttach.callCount, 1, 'SourceMapFailedToAttach events'); |
| sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client)); |
| }); |
| |
| it('triggers the correct lifecycle events when disabling once attached', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| const sourceMap = await sourceMapManager.sourceMapForClientPromise(client); |
| const sourceMapDetached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached); |
| |
| sourceMapManager.setEnabled(false); |
| |
| assert.strictEqual(sourceMapDetached.callCount, 1, 'SourceMapDetached events'); |
| sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.client', client)); |
| sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.sourceMap', sourceMap)); |
| }); |
| |
| it('triggers the correct lifecycle events when re-enabling', async () => { |
| const target = createTarget(); |
| const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target); |
| const client = new MockClient(target); |
| sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({content}); |
| sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL); |
| await sourceMapManager.sourceMapForClientPromise(client); |
| sourceMapManager.setEnabled(false); |
| const sourceMapDetached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached); |
| const sourceMapWillAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, sourceMapWillAttach); |
| const sourceMapFailedToAttach = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach); |
| const sourceMapAttached = sinon.spy(); |
| sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, sourceMapAttached); |
| |
| sourceMapManager.setEnabled(true); |
| |
| const sourceMap = await sourceMapManager.sourceMapForClientPromise(client); |
| assert.strictEqual(sourceMapDetached.callCount, 0, 'SourceMapDetached events'); |
| assert.strictEqual(sourceMapFailedToAttach.callCount, 0, 'SourceMapFailedToAttach events'); |
| assert.strictEqual(sourceMapWillAttach.callCount, 1, 'SourceMapWillAttach events'); |
| sinon.assert.calledWith(sourceMapWillAttach, sinon.match.hasNested('data.client', client)); |
| assert.isTrue(sourceMapAttached.calledAfter(sourceMapWillAttach)); |
| assert.strictEqual(sourceMapAttached.callCount, 1, 'SourceMapAttached events'); |
| sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.client', client)); |
| sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.sourceMap', sourceMap)); |
| }); |
| }); |
| }); |