blob: 02cf9624dbdb1115b3fc2002835b468a5d7778e7 [file] [log] [blame]
// 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));
});
});
});