Bug 1620280 - [devtools] Implement and use a SOURCE resource. r=jdescottes,bomsy,nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D86916
This commit is contained in:
Alexandre Poirot 2020-10-01 14:22:21 +00:00
Родитель 7e77c58985
Коммит 4367d1b79f
18 изменённых файлов: 382 добавлений и 77 удалений

Просмотреть файл

@ -51,6 +51,7 @@ class DebuggerPanel {
client, client,
} = await this.panelWin.Debugger.bootstrap({ } = await this.panelWin.Debugger.bootstrap({
targetList: this.toolbox.targetList, targetList: this.toolbox.targetList,
resourceWatcher: this.toolbox.resourceWatcher,
devToolsClient: this.toolbox.target.client, devToolsClient: this.toolbox.target.client,
workers: { workers: {
sourceMaps: this.toolbox.sourceMapService, sourceMaps: this.toolbox.sourceMapService,

Просмотреть файл

@ -47,9 +47,7 @@ import { validateNavigateContext, ContextError } from "../../utils/context";
import type { import type {
Source, Source,
SourceActorId,
SourceId, SourceId,
ThreadId,
Context, Context,
OriginalSourceData, OriginalSourceData,
GeneratedSourceData, GeneratedSourceData,
@ -400,18 +398,3 @@ function checkNewSources(cx: Context, sources: Source[]) {
return sources; return sources;
}; };
} }
export function ensureSourceActor(
thread: ThreadId,
sourceActor: SourceActorId
) {
return async function({ dispatch, getState, client }: ThunkArgs) {
await sourceQueue.flush();
if (hasSourceActor(getState(), sourceActor)) {
return Promise.resolve();
}
const sources = await client.fetchThreadSources(thread);
await dispatch(newGeneratedSources(sources));
};
}

Просмотреть файл

@ -7,7 +7,6 @@
import type { Target } from "../client/firefox/types"; import type { Target } from "../client/firefox/types";
import type { Action, ThunkArgs } from "./types"; import type { Action, ThunkArgs } from "./types";
import { removeSourceActors } from "./source-actors"; import { removeSourceActors } from "./source-actors";
import { newGeneratedSources } from "./sources";
import { validateContext } from "../utils/context"; import { validateContext } from "../utils/context";
import { getContext, getThread, getSourceActorsForThread } from "../selectors"; import { getContext, getThread, getSourceActorsForThread } from "../selectors";
@ -20,19 +19,6 @@ export function addTarget(targetFront: Target) {
validateContext(getState(), cx); validateContext(getState(), cx);
dispatch(({ type: "INSERT_THREAD", cx, newThread: thread }: Action)); dispatch(({ type: "INSERT_THREAD", cx, newThread: thread }: Action));
// Fetch the sources and install breakpoints on any new workers.
try {
const sources = await client.fetchThreadSources(thread.actor);
validateContext(getState(), cx);
await dispatch(newGeneratedSources(sources));
} catch (e) {
// NOTE: This fails quietly because it is pretty easy for sources to
// throw during the fetch if their thread shuts down,
// which would cause test failures.
console.error(e);
}
}; };
} }

Просмотреть файл

@ -7,20 +7,27 @@
import { setupCommands, clientCommands } from "./firefox/commands"; import { setupCommands, clientCommands } from "./firefox/commands";
import { setupEvents, clientEvents } from "./firefox/events"; import { setupEvents, clientEvents } from "./firefox/events";
import { features, prefs } from "../utils/prefs"; import { features, prefs } from "../utils/prefs";
import { prepareSourcePayload } from "./firefox/create";
import sourceQueue from "../utils/source-queue";
let actions; let actions;
let targetList; let targetList;
export async function onConnect( export async function onConnect(
connection: any, connection: any,
_actions: Object _actions: Object,
store: any
): Promise<void> { ): Promise<void> {
const { devToolsClient, targetList: _targetList } = connection; const {
devToolsClient,
targetList: _targetList,
resourceWatcher,
} = connection;
actions = _actions; actions = _actions;
targetList = _targetList; targetList = _targetList;
setupCommands({ devToolsClient, targetList }); setupCommands({ devToolsClient, targetList });
setupEvents({ actions, devToolsClient }); setupEvents({ actions, devToolsClient, store, resourceWatcher });
const { targetFront } = targetList; const { targetFront } = targetList;
if (targetFront.isBrowsingContext || targetFront.isParentProcess) { if (targetFront.isBrowsingContext || targetFront.isParentProcess) {
targetList.listenForWorkers = true; targetList.listenForWorkers = true;
@ -36,6 +43,16 @@ export async function onConnect(
onTargetAvailable, onTargetAvailable,
onTargetDestroyed onTargetDestroyed
); );
await resourceWatcher.watchResources([resourceWatcher.TYPES.SOURCE], {
onAvailable: onSourceAvailable,
});
// Tests like browser_webconsole_eval_sources.js using viewSourceInDebugger
// are expecting to find sources in the debugger store immediately for existing sources.
// So flush the queue immediately after calling watchResources, which will
// process all existing sources.
await sourceQueue.flush();
} }
async function onTargetAvailable({ async function onTargetAvailable({
@ -112,4 +129,23 @@ function onTargetDestroyed({ targetFront }): void {
actions.removeTarget(targetFront); actions.removeTarget(targetFront);
} }
async function onSourceAvailable(sources) {
for (const source of sources) {
const threadFront = await source.targetFront.getFront("thread");
const frontendSource = prepareSourcePayload(threadFront, source);
// Use SourceQueue, which will throttle all incoming sources and only display merged set of
// action every 100ms. Unless SourceQueue.flush is manually called when:
// - the thread is paused and we have to retrieve the source immediately
// - after fetching all existing sources (tests expect sources to be in redux store immediately)
//
// This throttling code could probably be migrated in the ResourceWatcher API
// so that we use a generic throttling algorithm for all resources.
sourceQueue.queue({
type: "generated",
data: frontendSource,
});
}
}
export { clientCommands, clientEvents }; export { clientCommands, clientEvents };

Просмотреть файл

@ -4,7 +4,7 @@
// @flow // @flow
import { prepareSourcePayload, createThread, createFrame } from "./create"; import { createThread, createFrame } from "./create";
import { import {
addThreadEventListeners, addThreadEventListeners,
clientEvents, clientEvents,
@ -22,7 +22,6 @@ import type {
PendingLocation, PendingLocation,
Frame, Frame,
FrameId, FrameId,
GeneratedSourceData,
Script, Script,
SourceId, SourceId,
SourceActor, SourceActor,
@ -39,7 +38,6 @@ import type {
ThreadFront, ThreadFront,
ObjectFront, ObjectFront,
ExpressionResult, ExpressionResult,
SourcesPacket,
} from "./types"; } from "./types";
import type { EventListenerCategoryList } from "../../actions/types"; import type { EventListenerCategoryList } from "../../actions/types";
@ -396,14 +394,6 @@ function registerSourceActor(sourceActorId: string, sourceId: SourceId) {
sourceActors[sourceActorId] = sourceId; sourceActors[sourceActorId] = sourceId;
} }
async function getSources(
client: ThreadFront
): Promise<Array<GeneratedSourceData>> {
const { sources }: SourcesPacket = await client.getSources();
return sources.map(source => prepareSourcePayload(client, source));
}
async function toggleEventLogging(logEventBreakpoints: boolean) { async function toggleEventLogging(logEventBreakpoints: boolean) {
return forEachThread(thread => return forEachThread(thread =>
thread.toggleEventLogging(logEventBreakpoints) thread.toggleEventLogging(logEventBreakpoints)
@ -418,21 +408,6 @@ function getAllThreadFronts(): ThreadFront[] {
return fronts; return fronts;
} }
// Fetch the sources for all the targets
async function fetchSources(): Promise<Array<GeneratedSourceData>> {
let sources = [];
for (const threadFront of getAllThreadFronts()) {
sources = sources.concat(await getSources(threadFront));
}
return sources;
}
async function fetchThreadSources(
thread: string
): Promise<Array<GeneratedSourceData>> {
return getSources(lookupThreadFront(thread));
}
// Check if any of the targets were paused before we opened // Check if any of the targets were paused before we opened
// the debugger. If one is paused. Fake a `pause` RDP event // the debugger. If one is paused. Fake a `pause` RDP event
// by directly calling the client event listener. // by directly calling the client event listener.
@ -555,8 +530,6 @@ const clientCommands = {
getFrames, getFrames,
pauseOnExceptions, pauseOnExceptions,
toggleEventLogging, toggleEventLogging,
fetchSources,
fetchThreadSources,
checkIfAlreadyPaused, checkIfAlreadyPaused,
registerSourceActor, registerSourceActor,
addThread, addThread,

Просмотреть файл

@ -5,7 +5,6 @@
// @flow // @flow
import type { import type {
SourcePacket,
PausedPacket, PausedPacket,
ThreadFront, ThreadFront,
Target, Target,
@ -14,19 +13,23 @@ import type {
import Actions from "../../actions"; import Actions from "../../actions";
import { createPause, prepareSourcePayload } from "./create"; import { createPause } from "./create";
import sourceQueue from "../../utils/source-queue"; import sourceQueue from "../../utils/source-queue";
import { recordEvent } from "../../utils/telemetry"; import { recordEvent } from "../../utils/telemetry";
import { prefs } from "../../utils/prefs"; import { prefs } from "../../utils/prefs";
import { hasSourceActor } from "../../selectors";
import { stringToSourceActorId } from "../../reducers/source-actors";
type Dependencies = { type Dependencies = {
actions: typeof Actions, actions: typeof Actions,
devToolsClient: DevToolsClient, devToolsClient: DevToolsClient,
store: any,
}; };
let actions: typeof Actions; let actions: typeof Actions;
let isInterrupted: boolean; let isInterrupted: boolean;
let threadFrontListeners: WeakMap<ThreadFront, Array<Function>>; let threadFrontListeners: WeakMap<ThreadFront, Array<Function>>;
let store: any;
function addThreadEventListeners(thread: ThreadFront): void { function addThreadEventListeners(thread: ThreadFront): void {
const removeListeners = []; const removeListeners = [];
@ -53,6 +56,7 @@ function attachAllTargets(currentTarget: Target): boolean {
function setupEvents(dependencies: Dependencies): void { function setupEvents(dependencies: Dependencies): void {
actions = dependencies.actions; actions = dependencies.actions;
sourceQueue.initialize(actions); sourceQueue.initialize(actions);
store = dependencies.store;
threadFrontListeners = new WeakMap(); threadFrontListeners = new WeakMap();
} }
@ -78,10 +82,7 @@ async function paused(
if (packet.frame) { if (packet.frame) {
// When reloading we might receive a pause event before the // When reloading we might receive a pause event before the
// top frame's source has arrived. // top frame's source has arrived.
await actions.ensureSourceActor( await ensureSourceActor(packet.frame.where.actor);
threadFront.actorID,
packet.frame.where.actor
);
} }
const pause = createPause(threadFront.actor, packet); const pause = createPause(threadFront.actor, packet);
@ -102,17 +103,36 @@ function resumed(threadFront: ThreadFront): void {
actions.resumed(threadFront.actorID); actions.resumed(threadFront.actorID);
} }
function newSource(threadFront: ThreadFront, { source }: SourcePacket): void { /**
sourceQueue.queue({ * This method wait for the given source is registered in Redux store.
type: "generated", *
data: prepareSourcePayload(threadFront, source), * @param {String} sourceActor
}); * Actor ID of the source to be waiting for.
*/
async function ensureSourceActor(sourceActor: string) {
const sourceActorId = stringToSourceActorId(sourceActor);
if (!hasSourceActor(store.getState(), sourceActorId)) {
await new Promise(resolve => {
const unsubscribe = store.subscribe(check);
let currentState = null;
function check() {
const previousState = currentState;
currentState = store.getState().sourceActors.values;
if (previousState == currentState) {
return;
}
if (hasSourceActor(store.getState(), sourceActorId)) {
unsubscribe();
resolve();
}
}
});
}
} }
const clientEvents = { const clientEvents = {
paused, paused,
resumed, resumed,
newSource,
}; };
export { export {

Просмотреть файл

@ -87,7 +87,7 @@ export async function onConnect(
initialState initialState
); );
const connected = client.onConnect(connection, actions); const connected = client.onConnect(connection, actions, store);
await syncBreakpoints(); await syncBreakpoints();
syncXHRBreakpoints(); syncXHRBreakpoints();

Просмотреть файл

@ -15,11 +15,18 @@ function unmountRoot() {
} }
module.exports = { module.exports = {
bootstrap: ({ targetList, devToolsClient, workers, panel }: any) => bootstrap: ({
targetList,
resourceWatcher,
devToolsClient,
workers,
panel,
}: any) =>
onConnect( onConnect(
{ {
tab: { clientType: "firefox" }, tab: { clientType: "firefox" },
targetList, targetList,
resourceWatcher,
devToolsClient, devToolsClient,
}, },
workers, workers,

Просмотреть файл

@ -47,7 +47,11 @@ export function insertResources<R: ResourceBound>(
for (const resource of resources) { for (const resource of resources) {
const { id } = resource; const { id } = resource;
if (state.identity[id]) { if (state.identity[id]) {
throw new Error(`Resource "${id}" already exists, cannot insert`); throw new Error(
`Resource "${id}" already exists, cannot insert ${JSON.stringify(
resource
)}`
);
} }
if (state.values[id]) { if (state.values[id]) {
throw new Error( throw new Error(

Просмотреть файл

@ -15,6 +15,8 @@ add_task(async function() {
// Navigate to a content process URL and check that the sources tree updates // Navigate to a content process URL and check that the sources tree updates
await navigate(dbg, EXAMPLE_URL + "doc-scripts.html", "simple1.js"); await navigate(dbg, EXAMPLE_URL + "doc-scripts.html", "simple1.js");
info("Wait for all sources to be in the store");
await waitFor(() => dbg.selectors.getSourceCount() == 5);
is(dbg.selectors.getSourceCount(), 5, "5 sources are loaded."); is(dbg.selectors.getSourceCount(), 5, "5 sources are loaded.");
// Check that you can still break after target switching. // Check that you can still break after target switching.

Просмотреть файл

@ -11,6 +11,7 @@ DevToolsModules(
'network-events.js', 'network-events.js',
'platform-messages.js', 'platform-messages.js',
'root-node.js', 'root-node.js',
'source.js',
'stylesheet.js', 'stylesheet.js',
'websocket.js', 'websocket.js',
) )

Просмотреть файл

@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
ResourceWatcher,
} = require("devtools/shared/resources/resource-watcher");
/**
* Emit SOURCE resources, which represents a Javascript source and has the following attributes set on "available":
*
* - introductionType {null|String}: A string indicating how this source code was introduced into the system.
* This will typically be set to "scriptElement", "eval", ...
* But this may have many other values:
* https://searchfox.org/mozilla-central/rev/ac142717cc067d875e83e4b1316f004f6e063a46/dom/script/ScriptLoader.cpp#2628-2639
* https://searchfox.org/mozilla-central/search?q=symbol:_ZN2JS14CompileOptions19setIntroductionTypeEPKc&redirect=false
* https://searchfox.org/mozilla-central/rev/ac142717cc067d875e83e4b1316f004f6e063a46/devtools/server/actors/source.js#160-169
* - sourceMapBaseURL {String}: Base URL where to look for a source map.
* This isn't the source map URL.
* - sourceMapURL {null|String}: URL of the source map, if there is one.
* - url {null|String}: URL of the source, if it relates to a particular URL.
* Evaled sources won't have any related URL.
* - isBlackBoxed {Boolean}: Specifying whether the source actor's 'black-boxed' flag is set.
* - extensionName {null|String}: If the source comes from an add-on, the add-on name.
*/
module.exports = async function({
targetList,
targetFront,
isFissionEnabledOnContentToolbox,
onAvailable,
}) {
const isBrowserToolbox = targetList.targetFront.isParentProcess;
const isNonTopLevelFrameTarget =
!targetFront.isTopLevel &&
targetFront.targetType === targetList.TYPES.FRAME;
if (isBrowserToolbox && isNonTopLevelFrameTarget) {
// In the BrowserToolbox, non-top-level frame targets are already
// debugged via content-process targets.
return;
}
const threadFront = await targetFront.getFront("thread");
// Use a list of all notified SourceFront as we don't have a newSource event for all sources
// but we sometime get sources notified both via newSource event *and* sources() method...
// We store actor ID instead of SourceFront as it appears that multiple SourceFront for the same
// actor are created...
const sourcesActorIDCache = new Set();
// Forward new sources (but also existing ones, see next comment)
threadFront.on("newSource", ({ source }) => {
if (sourcesActorIDCache.has(source.actor)) {
return;
}
sourcesActorIDCache.add(source.actor);
// source is a SourceActor's form, add the resourceType attribute on it
source.resourceType = ResourceWatcher.TYPES.SOURCE;
onAvailable([source]);
});
// Forward already existing sources
// Note that calling `sources()` will end up emitting `newSource` event for all existing sources.
// But not in some cases, for example, when the thread is already paused.
// (And yes, it means that already existing sources can be transfered twice over the wire)
let { sources } = await threadFront.sources();
// Note that `sources()` doesn't encapsulate SourceFront into a `source` attribute
// while `newSource` event does.
sources = sources.filter(source => {
return !sourcesActorIDCache.has(source.actor);
});
for (const source of sources) {
sourcesActorIDCache.add(source.actor);
// source is a SourceActor's form, add the resourceType attribute on it
source.resourceType = ResourceWatcher.TYPES.SOURCE;
}
onAvailable(sources);
};

Просмотреть файл

@ -666,6 +666,9 @@ class ResourceWatcher {
const onAvailable = this._onResourceAvailable.bind(this, { targetFront }); const onAvailable = this._onResourceAvailable.bind(this, { targetFront });
const onDestroyed = this._onResourceDestroyed.bind(this, { targetFront }); const onDestroyed = this._onResourceDestroyed.bind(this, { targetFront });
const onUpdated = this._onResourceUpdated.bind(this, { targetFront }); const onUpdated = this._onResourceUpdated.bind(this, { targetFront });
if (!(resourceType in LegacyListeners)) {
throw new Error(`Missing legacy listener for ${resourceType}`);
}
return LegacyListeners[resourceType]({ return LegacyListeners[resourceType]({
targetList: this.targetList, targetList: this.targetList,
targetFront, targetFront,
@ -747,6 +750,7 @@ ResourceWatcher.TYPES = ResourceWatcher.prototype.TYPES = {
NETWORK_EVENT: "network-event", NETWORK_EVENT: "network-event",
WEBSOCKET: "websocket", WEBSOCKET: "websocket",
NETWORK_EVENT_STACKTRACE: "network-event-stacktrace", NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
SOURCE: "source",
}; };
module.exports = { ResourceWatcher }; module.exports = { ResourceWatcher };
@ -791,6 +795,8 @@ const LegacyListeners = {
.WEBSOCKET]: require("devtools/shared/resources/legacy-listeners/websocket"), .WEBSOCKET]: require("devtools/shared/resources/legacy-listeners/websocket"),
[ResourceWatcher.TYPES [ResourceWatcher.TYPES
.NETWORK_EVENT_STACKTRACE]: require("devtools/shared/resources/legacy-listeners/network-event-stacktraces"), .NETWORK_EVENT_STACKTRACE]: require("devtools/shared/resources/legacy-listeners/network-event-stacktraces"),
[ResourceWatcher.TYPES
.SOURCE]: require("devtools/shared/resources/legacy-listeners/source"),
}; };
// Optional transformers for each type of resource. // Optional transformers for each type of resource.

Просмотреть файл

@ -9,6 +9,8 @@ support-files =
network_document.html network_document.html
fission_document.html fission_document.html
fission_iframe.html fission_iframe.html
sources.html
sources.js
style_document.css style_document.css
style_document.html style_document.html
style_iframe.css style_iframe.css
@ -19,6 +21,7 @@ support-files =
test_worker.js test_worker.js
websocket_backend_wsh.py websocket_backend_wsh.py
websocket_frontend.html websocket_frontend.html
worker-sources.js
[browser_resources_client_caching.js] [browser_resources_client_caching.js]
[browser_resources_console_messages.js] [browser_resources_console_messages.js]
@ -33,6 +36,7 @@ skip-if = os == "linux" #Bug 1655183
[browser_resources_platform_messages.js] [browser_resources_platform_messages.js]
[browser_resources_root_node.js] [browser_resources_root_node.js]
[browser_resources_several_resources.js] [browser_resources_several_resources.js]
[browser_resources_sources.js]
[browser_resources_stylesheets.js] [browser_resources_stylesheets.js]
[browser_resources_target_destroy.js] [browser_resources_target_destroy.js]
[browser_resources_target_resources_race.js] [browser_resources_target_resources_race.js]

Просмотреть файл

@ -0,0 +1,176 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the ResourceWatcher API around SOURCE.
const {
ResourceWatcher,
} = require("devtools/shared/resources/resource-watcher");
const TEST_URL = URL_ROOT + "sources.html";
add_task(async function() {
const tab = await addTab(TEST_URL);
const htmlRequest = await fetch(TEST_URL);
const htmlContent = await htmlRequest.text();
const {
client,
resourceWatcher,
targetList,
} = await initResourceWatcherAndTarget(tab);
// Force the target list to cover workers
targetList.listenForWorkers = true;
await targetList.startListening();
const targets = [];
await targetList.watchTargets(targetList.ALL_TYPES, async function({
targetFront,
}) {
targets.push(targetFront);
});
is(targets.length, 2, "Got expected number of targets");
info("Check already available resources");
const availableResources = [];
await resourceWatcher.watchResources([ResourceWatcher.TYPES.SOURCE], {
onAvailable: resources => availableResources.push(...resources),
});
const expectedExistingResources = [
{
description: "independent js file",
sourceForm: {
introductionType: "scriptElement",
sourceMapBaseURL:
"http://example.com/browser/devtools/shared/resources/tests/sources.js",
url:
"http://example.com/browser/devtools/shared/resources/tests/sources.js",
isBlackBoxed: false,
sourceMapURL: null,
extensionName: null,
},
sourceContent: {
contentType: "text/javascript",
source: "/* eslint-disable */\nfunction scriptSource() {}\n",
},
},
{
description: "eval",
sourceForm: {
introductionType: "eval",
sourceMapBaseURL:
"http://example.com/browser/devtools/shared/resources/tests/sources.html",
url: null,
isBlackBoxed: false,
sourceMapURL: null,
extensionName: null,
},
sourceContent: {
contentType: "text/javascript",
source: "this.global = function evalFunction() {}",
},
},
{
description: "inline JS",
sourceForm: {
introductionType: "scriptElement",
sourceMapBaseURL:
"http://example.com/browser/devtools/shared/resources/tests/sources.html",
url:
"http://example.com/browser/devtools/shared/resources/tests/sources.html",
isBlackBoxed: false,
sourceMapURL: null,
extensionName: null,
},
sourceContent: {
contentType: "text/html",
source: htmlContent,
},
},
{
description: "worker script",
sourceForm: {
introductionType: undefined,
sourceMapBaseURL:
"http://example.com/browser/devtools/shared/resources/tests/worker-sources.js",
url:
"http://example.com/browser/devtools/shared/resources/tests/worker-sources.js",
isBlackBoxed: false,
sourceMapURL: null,
extensionName: null,
},
sourceContent: {
contentType: "text/javascript",
source: "/* eslint-disable */\nfunction workerSource() {}\n",
},
},
];
await assertResources(availableResources, expectedExistingResources);
await targetList.stopListening();
await client.close();
});
async function assertResources(resources, expected) {
is(
resources.length,
expected.length,
"Length of existing resources is correct at initial"
);
for (let i = 0; i < resources.length; i++) {
await assertResource(resources[i], expected);
}
}
async function assertResource(source, expected) {
info(`Checking resource "#${expected.description}"`);
is(
source.resourceType,
ResourceWatcher.TYPES.SOURCE,
"Resource type is correct"
);
const threadFront = await source.targetFront.getFront("thread");
// `source` is SourceActor's form()
// so try to instantiate the related SourceFront:
const sourceFront = threadFront.source(source);
// then fetch source content
const sourceContent = await sourceFront.source();
// Order of sources is random, so we have to find the best expected resource.
// The only unique attribute is the JS Source text content.
const matchingExpected = expected.find(res => {
return res.sourceContent.source == sourceContent.source;
});
ok(
matchingExpected,
`This source was expected with source content being "${sourceContent.source}"`
);
assertObject(
sourceContent,
matchingExpected.sourceContent,
matchingExpected.description
);
assertObject(
source,
matchingExpected.sourceForm,
matchingExpected.description
);
}
function assertObject(object, expected, description) {
for (const field in expected) {
is(
object[field],
expected[field],
`The value of ${field} is correct for "#${description}"`
);
}
}

Просмотреть файл

@ -0,0 +1,21 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype HTML>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<script type="text/javascript">
"use strict";
/* eslint-disable */
function inlineSource() {}
// Assign it to a global in order to avoid it being GCed
eval("this.global = function evalFunction() {}");
// Assign the worker to a global variable in order to avoid
// having it be GCed.
this.worker = new Worker("worker-sources.js");
</script>
<script src="sources.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,2 @@
/* eslint-disable */
function scriptSource() {}

Просмотреть файл

@ -0,0 +1,2 @@
/* eslint-disable */
function workerSource() {}