Bug 1471754 - Implement the TargetList component. r=jdescottes

This component will help build and maintain the list of all the Targets.
Making it easier to:
* listen for all the targets: TargetList.watchTargets/unwatchTargets,
* iterate over all the existing ones: TargetList.getAllTargets,
* get all the TargetScoped fronts of all the targets: TargetList.getAllFronts.

Differential Revision: https://phabricator.services.mozilla.com/D48857

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Alexandre Poirot 2019-11-04 11:03:58 +00:00
Родитель a25daf5a9e
Коммит 19707475af
14 изменённых файлов: 1449 добавлений и 1 удалений

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

@ -54,6 +54,10 @@ class ProcessDescriptorFront extends FrontClassWithSpec(processDescriptorSpec) {
return front;
}
getCachedTarget() {
return this._processTargetFront;
}
async getTarget() {
// Only return the cached Target if it is still alive.
if (this._processTargetFront && this._processTargetFront.actorID) {
@ -89,7 +93,10 @@ class ProcessDescriptorFront extends FrontClassWithSpec(processDescriptorSpec) {
}
destroy() {
this._processTargetFront = null;
if (this._processTargetFront) {
this._processTargetFront.destroy();
this._processTargetFront = null;
}
this._targetFrontPromise = null;
super.destroy();
}

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

@ -23,6 +23,7 @@ DIRS += [
'platform',
'protocol',
'qrcode',
'resources',
'screenshot',
'security',
'sprintfjs',

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

@ -0,0 +1,9 @@
# 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/.
DevToolsModules(
'target-list.js',
)
BROWSER_CHROME_MANIFESTS += ['tests/browser.ini']

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

@ -0,0 +1,499 @@
/* 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 Services = require("Services");
const EventEmitter = require("devtools/shared/event-emitter");
const BROWSERTOOLBOX_FISSION_ENABLED = "devtools.browsertoolbox.fission";
// Intermediate components which implement the watch + unwatch
// using existing listFoo methods and fooListChanged events.
// The plan here is to followup to implement listen and unlisten
// methods directly on the target fronts. This code would then
// become the backward compatibility code which we could later remove.
class LegacyImplementationProcesses {
constructor(rootFront, target, onTargetAvailable, onTargetDestroyed) {
this.rootFront = rootFront;
this.target = target;
this.onTargetAvailable = onTargetAvailable;
this.onTargetDestroyed = onTargetDestroyed;
this.descriptors = new Set();
this._processListChanged = this._processListChanged.bind(this);
}
async _processListChanged() {
const { processes } = await this.rootFront.listProcesses();
// Process the new list to detect the ones being destroyed
// Force destroyed the descriptor as well as the target
for (const descriptor of this.descriptors) {
if (!processes.includes(descriptor)) {
// Manually call onTargetDestroyed listeners in order to
// ensure calling them *before* destroying the descriptor.
// Otherwise the descriptor will automatically destroy the target
// and may not fire the contentProcessTarget's destroy event.
const target = descriptor.getCachedTarget();
if (target) {
this.onTargetDestroyed(target);
}
descriptor.destroy();
this.descriptors.delete(descriptor);
}
}
// Add the new process descriptors to the local list
for (const descriptor of processes) {
if (!this.descriptors.has(descriptor)) {
this.descriptors.add(descriptor);
const target = await descriptor.getTarget();
if (!target) {
console.error("Wasn't able to retrieve the target for", target);
return;
}
this.onTargetAvailable(target);
}
}
}
async listen() {
this.rootFront.on("processListChanged", this._processListChanged);
await this._processListChanged();
}
unlisten() {
this.rootFront.off("processListChanged", this._processListChanged);
}
}
class LegacyImplementationFrames {
constructor(rootFront, target, onTargetAvailable) {
this.rootFront = rootFront;
this.target = target;
this.onTargetAvailable = onTargetAvailable;
}
async listen() {
// Note that even if we are calling listRemoteFrames on `this.target`, this ends up
// being forwarded to the RootFront. So that the Descriptors are managed
// by RootFront.
// TODO: support frame listening. For now, this only fetches already existing targets
const { frames } = await this.target.listRemoteFrames();
for (const frame of frames) {
// As we listen for frameDescriptor's on the RootFront, we get
// all the frames and not only the one related to the given `target`.
// TODO: support deeply nested frames
if (
frame.parentID == this.target.browsingContextID ||
frame.id == this.target.browsingContextID
) {
const target = await frame.getTarget();
if (!target) {
console.error("Wasn't able to retrieve the target for", frame);
continue;
}
this.onTargetAvailable(target);
}
}
}
unlisten() {}
}
// Note that in case we need to listen for all type of workers,
// devtools/client/shared/workers-listener.js already implements such listening.
class LegacyImplementationWorkers {
constructor(rootFront, target, onTargetAvailable, onTargetDestroyed) {
this.rootFront = rootFront;
this.target = target;
this.onTargetAvailable = onTargetAvailable;
this.onTargetDestroyed = onTargetDestroyed;
this.targets = new Set();
this._workerListChanged = this._workerListChanged.bind(this);
}
async _workerListChanged() {
const { workers } = await this.target.listWorkers();
// Process the new list to detect the ones being destroyed
// Force destroying the targets
for (const target of this.targets) {
if (!workers.includes(target)) {
this.onTargetDestroyed(target);
target.destroy();
this.targets.delete(target);
}
}
// Add the new worker targets to the local list
for (const target of workers) {
if (!this.targets.has(target)) {
this.targets.add(target);
this.onTargetAvailable(target);
}
}
}
async listen() {
this.target.on("workerListChanged", this._workerListChanged);
await this._workerListChanged();
}
unlisten() {
this.target.off("workerListChanged", this._workerListChanged);
}
}
class TargetList {
/**
* This class helps managing, iterating over and listening for Targets.
*
* It exposes:
* - the top level target, typically the main process target for the browser toolbox
* or the browsing context target for a regular web toolbox
* - target of remoted iframe, in case Fission is enabled and some <iframe>
* are running in a distinct process
* - target switching. If the top level target changes for a new one,
* all the targets are going to be declared as destroyed and the new ones
* will be notified to the user of this API.
*
* @param {RootFront} rootFront
* The root front.
* @param {TargetFront} targetFront
* The top level target to debug. Note that in case of target switching,
* this may be replaced by a new one over time.
*/
constructor(rootFront, targetFront) {
this.rootFront = rootFront;
// Note that this is a public attribute, used outside of this class
// and helps knowing what is the current top level target we debug.
this.targetFront = targetFront;
// Reports if we have at least one listener for the given target type
this._listenersStarted = new Set();
// List of all the target fronts
this._targets = new Set();
this._targets.add(targetFront);
// Listeners for target creation and destruction
this._createListeners = new EventEmitter();
this._destroyListeners = new EventEmitter();
this._onTargetAvailable = this._onTargetAvailable.bind(this);
this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
this.legacyImplementation = {
process: new LegacyImplementationProcesses(
this.rootFront,
this.targetFront,
this._onTargetAvailable,
this._onTargetDestroyed
),
frame: new LegacyImplementationFrames(
this.rootFront,
this.targetFront,
this._onTargetAvailable,
this._onTargetDestroyed
),
worker: new LegacyImplementationWorkers(
this.rootFront,
this.targetFront,
this._onTargetAvailable,
this._onTargetDestroyed
),
};
}
_fissionEnabled() {
const fissionBrowserToolboxEnabled = Services.prefs.getBoolPref(
BROWSERTOOLBOX_FISSION_ENABLED
);
const isParentProcessToolboxOrBrowserConsole =
this.targetFront.chrome && !this.targetFront.isAddon;
return (
fissionBrowserToolboxEnabled && isParentProcessToolboxOrBrowserConsole
);
}
// Called whenever a new Target front is available.
// Either because a target was already available as we started calling startListening
// or if it has just been created
async _onTargetAvailable(targetFront) {
if (this._targets.has(targetFront)) {
console.error(
"Target is already registered in the TargetList",
targetFront
);
return;
}
this._targets.add(targetFront);
// Map the descriptor typeName to a target type.
const targetType = this._getTargetType(targetFront);
// Notify the target front creation listeners
this._createListeners.emit(
targetType,
targetType,
targetFront,
targetFront == this.targetFront
);
}
_onTargetDestroyed(targetFront) {
const targetType = this._getTargetType(targetFront);
this._destroyListeners.emit(
targetType,
targetType,
targetFront,
targetFront == this.targetFront
);
this._targets.delete(targetFront);
}
_setListening(type, value) {
if (value) {
this._listenersStarted.add(type);
} else {
this._listenersStarted.delete(type);
}
}
_isListening(type) {
return this._listenersStarted.has(type);
}
/**
* Interact with the actors in order to start listening for new types of targets.
* This will fire the _onTargetAvailable function for all already-existing targets,
* as well as the next one to be created. It will also call _onTargetDestroyed
* everytime a target is reported as destroyed by the actors.
* By the time this function resolves, all the already-existing targets will be
* reported to _onTargetAvailable.
*/
async startListening(types) {
for (const type of types) {
if (this._isListening(type)) {
continue;
}
this._setListening(type, true);
// We only listen for additional target when the fission pref is turned on.
if (!this._fissionEnabled()) {
continue;
}
if (this.legacyImplementation[type]) {
await this.legacyImplementation[type].listen();
} else {
// TO BE IMPLEMENTED via this.targetFront.watchFronts(type)
// For now we always go throught "legacy" codepath.
throw new Error(`Unsupported target type '${type}'`);
}
}
}
stopListening(types) {
for (const type of types) {
if (!this._isListening(type)) {
continue;
}
this._setListening(type, false);
// We only listen for additional target when the fission pref is turned on.
if (!this._fissionEnabled()) {
continue;
}
if (this.legacyImplementation[type]) {
this.legacyImplementation[type].unlisten();
} else {
// TO BE IMPLEMENTED via this.targetFront.unwatchFronts(type)
// For now we always go throught "legacy" codepath.
throw new Error(`Unsupported target type '${type}'`);
}
}
}
_getTargetType(target) {
const { typeName } = target;
if (typeName == "browsingContextTarget") {
return TargetList.TYPES.FRAME;
} else if (
typeName == "contentProcessTarget" ||
typeName == "parentProcessTarget"
) {
return TargetList.TYPES.PROCESS;
} else if (typeName == "workerTarget") {
return TargetList.TYPES.WORKER;
}
throw new Error("Unsupported target typeName: " + typeName);
}
_matchTargetType(type, target) {
return type === this._getTargetType(target);
}
/**
* Listen for the creation and/or destruction of target fronts matching one of the provided types.
*
* @param {Array<String>} types
* The type of target to listen for. Constant of TargetList.TYPES.
* @param {Function} onAvailable
* Callback fired when a target has been just created or was already available.
* The function is called with three arguments:
* - {String} type: The target type
* - {TargetFront} target: The target Front
* - {Boolean} isTopLevel: Is this target the top level one?
* @param {Function} onDestroy
* Callback fired in case of target front destruction.
* The function is called with the same arguments than onAvailable.
*/
async watchTargets(types, onAvailable, onDestroy) {
if (typeof onAvailable != "function") {
throw new Error(
"TargetList.watchTargets expects a function as second argument"
);
}
for (const type of types) {
if (!this._isListening(type)) {
throw new Error(
`watchTargets was called for a target type (${type}) that isn't being listened to`
);
}
// Notify about already existing target of these types
for (const target of this._targets) {
if (this._matchTargetType(type, target)) {
try {
// Ensure waiting for eventual async create listeners
// which may setup things regarding the existing targets
// and listen callsite may care about the full initialization
await onAvailable(type, target, target == this.targetFront);
} catch (e) {
// Prevent throwing when onAvailable handler throws on one target
// so that it can try to register the other targets
console.error(
"Exception when calling onAvailable handler",
e.message,
e
);
}
}
}
this._createListeners.on(type, onAvailable);
if (onDestroy) {
this._destroyListeners.on(type, onDestroy);
}
}
}
/**
* Stop listening for the creation and/or destruction of a given type of target fronts.
* See `watchTargets()` for documentation of the arguments.
*/
async unwatchTargets(types, onAvailable, onDestroy) {
if (typeof onAvailable != "function") {
throw new Error(
"TargetList.unwatchTargets expects a function as second argument"
);
}
for (const type of types) {
this._createListeners.off(type, onAvailable);
if (onDestroy) {
this._destroyListeners.off(type, onDestroy);
}
}
}
/**
* Retrieve all the current target fronts of a given type.
*
* @param {String} type
* The type of target to retrieve. Constant of TargetList.TYPES.
*/
getAllTargets(type) {
if (!type) {
throw new Error("getAllTargets expects a 'type' argument");
}
if (!this._isListening(type)) {
throw new Error(
`getAllTargets was called for a target type (${type}) that isn't being listened to`
);
}
const targets = [...this._targets].filter(target =>
this._matchTargetType(type, target)
);
return targets;
}
/**
* For all the target fronts of a given type, retrieve all the target-scoped fronts of a given type.
*
* @param {String} targetType
* The type of target to iterate over. Constant of TargetList.TYPES.
* @param {String} frontType
* The type of target-scoped front to retrieve. It can be "inspector", "console", "thread",...
*/
async getAllFronts(targetType, frontType) {
const fronts = [];
const targets = this.getAllTargets(targetType);
for (const target of targets) {
const front = await target.getFront(frontType);
fronts.push(front);
}
return fronts;
}
/**
* Called when the top level target is replaced by a new one.
* Typically when we navigate to another domain which requires to be loaded in a distinct process.
*
* @param {TargetFront} newTarget
* The new top level target to debug.
*/
async switchToTarget(newTarget) {
// First report that all existing targets are destroyed
for (const target of this._targets) {
this._onTargetDestroyed(target);
}
const listenedTypes = TargetList.ALL_TYPES.filter(type =>
this._isListening(type)
);
this.stopListening(listenedTypes);
// Clear the cached target list
this._targets.clear();
// Update the reference to the top level target so that
// creation listening can know this is about the top level target
this.targetFront = newTarget;
// Notify about this new target to creation listeners
this._onTargetAvailable(newTarget);
// Re-register the listeners as the top level target changed
// and some targets are fetched from it
await this.startListening(listenedTypes);
}
}
/**
* All types of target:
*/
TargetList.TYPES = TargetList.prototype.TYPES = {
PROCESS: "process",
FRAME: "frame",
WORKER: "worker",
};
TargetList.ALL_TYPES = TargetList.prototype.ALL_TYPES = Object.values(
TargetList.TYPES
);
module.exports = { TargetList };

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

@ -0,0 +1,6 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests.js"
};

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

@ -0,0 +1,15 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
head.js
fission_document.html
fission_iframe.html
[browser_target_list_frames.js]
[browser_target_list_preffedoff.js]
[browser_target_list_processes.js]
[browser_target_list_switchToTarget.js]
[browser_target_list_watchTargets.js]

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

@ -0,0 +1,162 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the TargetList API around frames
const { TargetList } = require("devtools/shared/resources/target-list");
const FISSION_TEST_URL = URL_ROOT + "/fission_document.html";
add_task(async function() {
// Enabled fission's pref as the TargetList is almost disabled without it
await pushPref("devtools.browsertoolbox.fission", true);
// Disable the preloaded process as it gets created lazily and may interfere
// with process count assertions
await pushPref("dom.ipc.processPrelaunch.enabled", false);
// This preference helps destroying the content process when we close the tab
await pushPref("dom.ipc.keepProcessesAlive.web", 1);
const client = await createLocalClient();
const mainRoot = client.mainRoot;
await testBrowserFrames(mainRoot);
await testTabFrames(mainRoot);
await client.close();
});
async function testBrowserFrames(mainRoot) {
info("Test TargetList against frames via the parent process target");
const target = await mainRoot.getMainProcess();
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([TargetList.TYPES.FRAME]);
// Very naive sanity check against getAllTargets(frame)
const frames = await targetList.getAllTargets(TargetList.TYPES.FRAME);
const hasBrowserDocument = frames.find(
frameTarget => frameTarget.url == window.location.href
);
ok(hasBrowserDocument, "retrieve the target for the browser document");
// Check that calling getAllTargets(frame) return the same target instances
const frames2 = await targetList.getAllTargets(TargetList.TYPES.FRAME);
is(frames2.length, frames.length, "retrieved the same number of frames");
function sortFronts(f1, f2) {
return f1.actorID < f2.actorID;
}
frames.sort(sortFronts);
frames2.sort(sortFronts);
for (let i = 0; i < frames.length; i++) {
is(frames[i], frames2[i], `frame ${i} targets are the same`);
}
// Assert that watchTargets will call the create callback for all existing frames
const targets = [];
const onAvailable = (type, newTarget, isTopLevel) => {
is(
type,
TargetList.TYPES.FRAME,
"We are only notified about frame targets"
);
ok(
newTarget == target ? isTopLevel : !isTopLevel,
"isTopLevel argument is correct"
);
targets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.FRAME], onAvailable);
is(
targets.length,
frames.length,
"retrieved the same number of frames via watchTargets"
);
frames.sort(sortFronts);
targets.sort(sortFronts);
for (let i = 0; i < frames.length; i++) {
is(
frames[i],
targets[i],
`frame ${i} targets are the same via watchTargets`
);
}
targetList.unwatchTargets([TargetList.TYPES.FRAME], onAvailable);
/* NOT READY YET, need to implement frame listening
// Open a new tab and see if the frame target is reported by watchTargets and getAllTargets
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
const tab = await addTab(TEST_URL);
is(targets.length, frames.length + 1, "Opening a tab reported a new frame");
is(targets[targets.length - 1].url, TEST_URL, "This frame target is about the new tab");
const frames3 = await targetList.getAllTargets(TargetList.TYPES.FRAME);
const hasTabDocument = frames3.find(target => target.url == TEST_URL);
ok(hasTabDocument, "retrieve the target for tab via getAllTargets");
*/
targetList.stopListening([TargetList.TYPES.FRAME]);
}
// For now as we do not support "real fission", for tabs, this behaves as if devtools fission pref was false
async function testTabFrames(mainRoot) {
info("Test TargetList against frames via a tab target");
// Create a TargetList for a given test tab
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
const tab = await addTab(FISSION_TEST_URL);
const target = await mainRoot.getTab({ tab });
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([TargetList.TYPES.FRAME]);
// Check that calling getAllTargets(frame) return the same target instances
const frames = await targetList.getAllTargets(TargetList.TYPES.FRAME);
is(
frames.length,
1,
"retrieved the top level document and the remoted frame"
);
is(
frames[0].url,
FISSION_TEST_URL,
"The first frame is the top level document"
);
// Assert that watchTargets will call the create callback for all existing frames
const targets = [];
const onAvailable = (type, newTarget, isTopLevel) => {
is(
type,
TargetList.TYPES.FRAME,
"We are only notified about frame targets"
);
ok(
newTarget == target ? isTopLevel : !isTopLevel,
"isTopLevel argument is correct"
);
targets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.FRAME], onAvailable);
is(
targets.length,
frames.length,
"retrieved the same number of frames via watchTargets"
);
for (let i = 0; i < frames.length; i++) {
is(
frames[i],
targets[i],
`frame ${i} targets are the same via watchTargets`
);
}
targetList.unwatchTargets([TargetList.TYPES.FRAME], onAvailable);
targetList.stopListening([TargetList.TYPES.FRAME]);
BrowserTestUtils.removeTab(tab);
}

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

@ -0,0 +1,146 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the TargetList API when DevTools Fission preference is false
const { TargetList } = require("devtools/shared/resources/target-list");
const FISSION_TEST_URL = URL_ROOT + "/fission_document.html";
add_task(async function() {
// Disable the preloaded process as it gets created lazily and may interfere
// with process count assertions
await pushPref("dom.ipc.processPrelaunch.enabled", false);
// This preference helps destroying the content process when we close the tab
await pushPref("dom.ipc.keepProcessesAlive.web", 1);
const client = await createLocalClient();
const mainRoot = client.mainRoot;
const mainProcess = await mainRoot.getMainProcess();
// Assert the limited behavior of this API with fission preffed off
await pushPref("devtools.browsertoolbox.fission", false);
await testPreffedOffMainProcess(mainRoot, mainProcess);
await testPreffedOffTab(mainRoot);
await client.close();
});
async function testPreffedOffMainProcess(mainRoot, mainProcess) {
info(
"Test TargetList when devtools's fission pref is false, via the parent process target"
);
const targetList = new TargetList(mainRoot, mainProcess);
await targetList.startListening([
TargetList.TYPES.PROCESS,
TargetList.TYPES.FRAME,
]);
// The API should only report the top level target,
// i.e. the Main process target, which is considered as frame
// and not as process.
const processes = await targetList.getAllTargets(TargetList.TYPES.PROCESS);
is(processes.length, 0);
const frames = await targetList.getAllTargets(TargetList.TYPES.FRAME);
is(frames.length, 1, "We get only one frame when preffed-off");
is(
frames[0],
mainProcess,
"The target is the top level one via getAllTargets"
);
const processTargets = [];
const onProcessAvailable = (type, newTarget, isTopLevel) => {
processTargets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.PROCESS], onProcessAvailable);
is(processTargets.length, 0, "We get no process when preffed-off");
targetList.unwatchTargets([TargetList.TYPES.PROCESS], onProcessAvailable);
const frameTargets = [];
const onFrameAvailable = (type, newTarget, isTopLevel) => {
is(
type,
TargetList.TYPES.FRAME,
"We are only notified about frame targets"
);
ok(isTopLevel, "We are only notified about the top level target");
frameTargets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
is(
frameTargets.length,
1,
"We get one frame via watchTargets when preffed-off"
);
is(
frameTargets[0],
mainProcess,
"The target is the top level one via watchTargets"
);
targetList.unwatchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
targetList.stopListening([TargetList.TYPES.PROCESS, TargetList.TYPES.FRAME]);
}
async function testPreffedOffTab(mainRoot) {
info(
"Test TargetList when devtools's fission pref is false, via the tab target"
);
// Create a TargetList for a given test tab
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
const tab = await addTab(FISSION_TEST_URL);
const target = await mainRoot.getTab({ tab });
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([
TargetList.TYPES.PROCESS,
TargetList.TYPES.FRAME,
]);
const processes = await targetList.getAllTargets(TargetList.TYPES.PROCESS);
is(processes.length, 0);
// This only reports the top level target when devtools fission preference is off
const frames = await targetList.getAllTargets(TargetList.TYPES.FRAME);
is(frames.length, 1, "We get only one frame when preffed-off");
is(frames[0], target, "The target is the top level one via getAllTargets");
const processTargets = [];
const onProcessAvailable = newTarget => {
processTargets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.PROCESS], onProcessAvailable);
is(processTargets.length, 0, "We get no process when preffed-off");
targetList.unwatchTargets([TargetList.TYPES.PROCESS], onProcessAvailable);
const frameTargets = [];
const onFrameAvailable = (type, newTarget, isTopLevel) => {
is(
type,
TargetList.TYPES.FRAME,
"We are only notified about frame targets"
);
ok(isTopLevel, "We are only notified about the top level target");
frameTargets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
is(
frameTargets.length,
1,
"We get one frame via watchTargets when preffed-off"
);
is(
frameTargets[0],
target,
"The target is the top level one via watchTargets"
);
targetList.unwatchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
targetList.stopListening([TargetList.TYPES.PROCESS, TargetList.TYPES.FRAME]);
BrowserTestUtils.removeTab(tab);
}

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

@ -0,0 +1,187 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the TargetList API around processes
const { TargetList } = require("devtools/shared/resources/target-list");
const TEST_URL =
"data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
add_task(async function() {
// Enabled fission's pref as the TargetList is almost disabled without it
await pushPref("devtools.browsertoolbox.fission", true);
// Disable the preloaded process as it gets created lazily and may interfere
// with process count assertions
await pushPref("dom.ipc.processPrelaunch.enabled", false);
// This preference helps destroying the content process when we close the tab
await pushPref("dom.ipc.keepProcessesAlive.web", 1);
const client = await createLocalClient();
const mainRoot = client.mainRoot;
const mainProcess = await mainRoot.getMainProcess();
const targetList = new TargetList(mainRoot, mainProcess);
await targetList.startListening([TargetList.TYPES.PROCESS]);
await testProcesses(targetList, mainProcess);
await targetList.stopListening([TargetList.TYPES.PROCESS]);
await client.close();
});
async function testProcesses(targetList, target) {
info("Test TargetList against processes");
// Note that ppmm also includes the parent process, which is considered as a frame rather than a process
const originalProcessesCount = Services.ppmm.childCount - 1;
const processes = await targetList.getAllTargets(TargetList.TYPES.PROCESS);
is(
processes.length,
originalProcessesCount,
"Get a target for all content processes"
);
const processes2 = await targetList.getAllTargets(TargetList.TYPES.PROCESS);
is(
processes2.length,
originalProcessesCount,
"retrieved the same number of processes"
);
function sortFronts(f1, f2) {
return f1.actorID < f2.actorID;
}
processes.sort(sortFronts);
processes2.sort(sortFronts);
for (let i = 0; i < processes.length; i++) {
is(processes[i], processes2[i], `process ${i} targets are the same`);
}
// Assert that watchTargets will call the create callback for all existing frames
const targets = new Set();
const onAvailable = (type, newTarget, isTopLevel) => {
if (targets.has(newTarget)) {
ok(false, "The same target is notified multiple times via onAvailable");
}
is(
type,
TargetList.TYPES.PROCESS,
"We are only notified about process targets"
);
ok(
newTarget == target ? isTopLevel : !isTopLevel,
"isTopLevel argument is correct"
);
targets.add(newTarget);
};
const onDestroyed = (type, newTarget, isTopLevel) => {
if (!targets.has(newTarget)) {
ok(
false,
"A target is declared destroyed via onDestroyed without being notified via onAvailable"
);
}
is(
type,
TargetList.TYPES.PROCESS,
"We are only notified about process targets"
);
ok(
!isTopLevel,
"We are never notified about the top level target destruction"
);
targets.delete(newTarget);
};
await targetList.watchTargets(
[TargetList.TYPES.PROCESS],
onAvailable,
onDestroyed
);
is(
targets.size,
originalProcessesCount,
"retrieved the same number of processes via watchTargets"
);
for (let i = 0; i < processes.length; i++) {
ok(
targets.has(processes[i]),
`process ${i} targets are the same via watchTargets`
);
}
const previousTargets = new Set(targets);
// Assert that onAvailable is called for processes created *after* the call to watchTargets
const onProcessCreated = new Promise(resolve => {
const onAvailable2 = (type, newTarget, isTopLevel) => {
if (previousTargets.has(newTarget)) {
return;
}
targetList.unwatchTargets([TargetList.TYPES.PROCESS], onAvailable2);
resolve(newTarget);
};
targetList.watchTargets([TargetList.TYPES.PROCESS], onAvailable2);
});
const tab1 = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: TEST_URL,
forceNewProcess: true,
});
const createdTarget = await onProcessCreated;
// For some reason, creating a new tab purges processes created from previous tests
// so it is not reasonable to assert the size of `targets` as it may be lower than expected.
ok(targets.has(createdTarget), "The new tab process is in the list");
const processCountAfterTabOpen = targets.size;
// Assert that onDestroyed is called for destroyed processes
const onProcessDestroyed = new Promise(resolve => {
const onAvailable3 = () => {};
const onDestroyed3 = (type, newTarget, isTopLevel) => {
resolve(newTarget);
targetList.unwatchTargets(
[TargetList.TYPES.PROCESS],
onAvailable3,
onDestroyed3
);
};
targetList.watchTargets(
[TargetList.TYPES.PROCESS],
onAvailable3,
onDestroyed3
);
});
BrowserTestUtils.removeTab(tab1);
const destroyedTarget = await onProcessDestroyed;
is(
targets.size,
processCountAfterTabOpen - 1,
"The closed tab's process has been reported as destroyed"
);
ok(
!targets.has(destroyedTarget),
"The destroyed target is no longer in the list"
);
is(
destroyedTarget,
createdTarget,
"The destroyed target is the one that has been reported as created"
);
await targetList.unwatchTargets(
[TargetList.TYPES.PROCESS],
onAvailable,
onDestroyed
);
// Ensure that getAllTargets still works after the call to unwatchTargets
const processes3 = await targetList.getAllTargets(TargetList.TYPES.PROCESS);
is(
processes3.length,
processCountAfterTabOpen - 1,
"getAllTargets reports a new target"
);
}

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

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the TargetList API switchToTarget function
const { TargetList } = require("devtools/shared/resources/target-list");
add_task(async function() {
// Enabled fission's pref as the TargetList is almost disabled without it
await pushPref("devtools.browsertoolbox.fission", true);
// Disable the preloaded process as it gets created lazily and may interfere
// with process count assertions
await pushPref("dom.ipc.processPrelaunch.enabled", false);
// This preference helps destroying the content process when we close the tab
await pushPref("dom.ipc.keepProcessesAlive.web", 1);
const client = await createLocalClient();
await testSwitchToTarget(client);
await client.close();
});
async function testSwitchToTarget(client) {
info("Test TargetList.switchToTarget method");
const { mainRoot } = client;
let target = await mainRoot.getMainProcess();
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([TargetList.TYPES.FRAME]);
is(
targetList.targetFront,
target,
"The target list top level target is the main process one"
);
// Create the new target to switch to, a new tab with an iframe
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
const tab = await addTab(
`data:text/html,<iframe src="data:text/html,foo"></iframe>`
);
const secondTarget = await mainRoot.getTab({ tab: gBrowser.selectedTab });
const frameTargets = [];
const onFrameAvailable = (type, newTarget, isTopLevel) => {
is(
type,
TargetList.TYPES.FRAME,
"We are only notified about frame targets"
);
ok(
newTarget == target ? isTopLevel : !isTopLevel,
"isTopLevel argument is correct"
);
frameTargets.push(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.FRAME], onFrameAvailable);
// Clear the recorded target list of all existing targets
frameTargets.length = 0;
target = secondTarget;
await targetList.switchToTarget(secondTarget);
is(
targetList.targetFront,
target,
"After the switch, the top level target has been updated"
);
// Because JS Window Actor API isn't used yet, FrameDescriptor.getTarget returns null
// And there is no target being created for the iframe, yet.
// As soon as bug 1565200 is resolved, this should return two frames, including the iframe.
is(
frameTargets.length,
1,
"We get the report of two iframe when switching to the new target"
);
is(frameTargets[0], target);
//is(frameTargets[1].url, "data:text/html,foo");
targetList.stopListening([TargetList.TYPES.FRAME]);
BrowserTestUtils.removeTab(tab);
}

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

@ -0,0 +1,273 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the TargetList's `watchTargets` function
const { TargetList } = require("devtools/shared/resources/target-list");
const TEST_URL =
"data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);
add_task(async function() {
// Enabled fission's pref as the TargetList is almost disabled without it
await pushPref("devtools.browsertoolbox.fission", true);
// Disable the preloaded process as it gets created lazily and may interfere
// with process count assertions
await pushPref("dom.ipc.processPrelaunch.enabled", false);
// This preference helps destroying the content process when we close the tab
await pushPref("dom.ipc.keepProcessesAlive.web", 1);
const client = await createLocalClient();
const mainRoot = client.mainRoot;
await testWatchTargets(mainRoot);
await testContentProcessTarget(mainRoot);
await testThrowingInOnAvailable(mainRoot);
await client.close();
});
async function testWatchTargets(mainRoot) {
info("Test TargetList watchTargets function");
const target = await mainRoot.getMainProcess();
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([TargetList.TYPES.PROCESS]);
// Note that ppmm also includes the parent process, which is considered as a frame rather than a process
const originalProcessesCount = Services.ppmm.childCount - 1;
info(
"Check that onAvailable is called for processes already created *before* the call to watchTargets"
);
const targets = new Set();
const onAvailable = (type, newTarget, isTopLevel) => {
if (targets.has(newTarget)) {
ok(false, "The same target is notified multiple times via onAvailable");
}
is(
type,
TargetList.TYPES.PROCESS,
"We are only notified about process targets"
);
ok(
newTarget == target ? isTopLevel : !isTopLevel,
"isTopLevel argument is correct"
);
targets.add(newTarget);
};
const onDestroyed = (type, newTarget, isTopLevel) => {
if (!targets.has(newTarget)) {
ok(
false,
"A target is declared destroyed via onDestroyed without being notified via onAvailable"
);
}
is(
type,
TargetList.TYPES.PROCESS,
"We are only notified about process targets"
);
ok(
!isTopLevel,
"We are not notified about the top level target destruction"
);
targets.delete(newTarget);
};
await targetList.watchTargets(
[TargetList.TYPES.PROCESS],
onAvailable,
onDestroyed
);
is(
targets.size,
originalProcessesCount,
"retrieved the expected number of processes via watchTargets"
);
// Start from 1 in order to ignore the parent process target, which is considered as a frame rather than a process
for (let i = 1; i < Services.ppmm.childCount; i++) {
const process = Services.ppmm.getChildAt(i);
const hasTargetWithSamePID = [...targets].find(
processTarget => processTarget.descriptorFront.id == process.osPid
);
ok(
hasTargetWithSamePID,
`Process with PID ${process.osPid} has been reported via onAvailable`
);
}
info(
"Check that onAvailable is called for processes created *after* the call to watchTargets"
);
const previousTargets = new Set(targets);
const onProcessCreated = new Promise(resolve => {
const onAvailable2 = (type, newTarget, isTopLevel) => {
if (previousTargets.has(newTarget)) {
return;
}
targetList.unwatchTargets([TargetList.TYPES.PROCESS], onAvailable2);
resolve(newTarget);
};
targetList.watchTargets([TargetList.TYPES.PROCESS], onAvailable2);
});
const tab1 = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: TEST_URL,
forceNewProcess: true,
});
const createdTarget = await onProcessCreated;
// For some reason, creating a new tab purges processes created from previous tests
// so it is not reasonable to assert the side of `targets` as it may be lower than expected.
ok(targets.has(createdTarget), "The new tab process is in the list");
const processCountAfterTabOpen = targets.size;
// Assert that onDestroyed is called for destroyed processes
const onProcessDestroyed = new Promise(resolve => {
const onAvailable3 = () => {};
const onDestroyed3 = (type, newTarget, isTopLevel) => {
resolve(newTarget);
targetList.unwatchTargets(
[TargetList.TYPES.PROCESS],
onAvailable3,
onDestroyed3
);
};
targetList.watchTargets(
[TargetList.TYPES.PROCESS],
onAvailable3,
onDestroyed3
);
});
BrowserTestUtils.removeTab(tab1);
const destroyedTarget = await onProcessDestroyed;
is(
targets.size,
processCountAfterTabOpen - 1,
"The closed tab's process has been reported as destroyed"
);
ok(
!targets.has(destroyedTarget),
"The destroyed target is no longer in the list"
);
is(
destroyedTarget,
createdTarget,
"The destroyed target is the one that has been reported as created"
);
await targetList.unwatchTargets(
[TargetList.TYPES.PROCESS],
onAvailable,
onDestroyed
);
targetList.stopListening([TargetList.TYPES.PROCESS]);
}
async function testContentProcessTarget(mainRoot) {
info("Test TargetList watchTargets with a content process target");
const { processes } = await mainRoot.listProcesses();
const target = await processes[1].getTarget();
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([TargetList.TYPES.PROCESS]);
// Note that ppmm also includes the parent process, which is considered as a frame rather than a process
const originalProcessesCount = Services.ppmm.childCount - 1;
// Assert that watchTargets will call the create callback for all existing frames
const targets = new Set();
const onAvailable = (type, newTarget, isTopLevel) => {
if (targets.has(newTarget)) {
// This may fail if the top level target is reported by LegacyImplementation
// to TargetList and emits an available event for it.
ok(false, "The same target is notified multiple times via onAvailable");
}
is(
type,
TargetList.TYPES.PROCESS,
"We are only notified about process targets"
);
ok(
newTarget == target ? isTopLevel : !isTopLevel,
"isTopLevel argument is correct"
);
targets.add(newTarget);
};
const onDestroyed = (type, newTarget, isTopLevel) => {
if (!targets.has(newTarget)) {
ok(
false,
"A target is declared destroyed via onDestroyed without being notified via onAvailable"
);
}
is(
type,
TargetList.TYPES.PROCESS,
"We are only notified about process targets"
);
ok(
!isTopLevel,
"We are not notified about the top level target destruction"
);
targets.delete(newTarget);
};
await targetList.watchTargets(
[TargetList.TYPES.PROCESS],
onAvailable,
onDestroyed
);
// This may fail if the top level target is reported by LegacyImplementation
// to TargetList and registers a duplicated entry
is(
targets.size,
originalProcessesCount,
"retrieved the same number of processes via watchTargets"
);
targetList.stopListening([TargetList.TYPES.PROCESS]);
}
async function testThrowingInOnAvailable(mainRoot) {
info(
"Test TargetList watchTargets function when an exception is thrown in onAvailable callback"
);
const target = await mainRoot.getMainProcess();
const targetList = new TargetList(mainRoot, target);
await targetList.startListening([TargetList.TYPES.PROCESS]);
// Note that ppmm also includes the parent process, which is considered as a frame rather than a process
const originalProcessesCount = Services.ppmm.childCount - 1;
info(
"Check that onAvailable is called for processes already created *before* the call to watchTargets"
);
const targets = new Set();
let thrown = false;
const onAvailable = (type, newTarget, isTopLevel) => {
if (!thrown) {
thrown = true;
throw new Error("Force an exception when processing the first target");
}
targets.add(newTarget);
};
await targetList.watchTargets([TargetList.TYPES.PROCESS], onAvailable);
is(
targets.size,
originalProcessesCount - 1,
"retrieved the expected number of processes via onAvailable. All but the first one where we have thrown."
);
targetList.stopListening([TargetList.TYPES.PROCESS]);
}

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

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf8">
<title>Test fission document</title>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test fission iframe</p>
<iframe src="https://example.com/browser/devtools/shared/resources/tests/fission_iframe.html"></iframe>
</body>
</html>

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

@ -0,0 +1,12 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf8">
<title>Test fission iframe document</title>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>remote iframe</p>
</body>
</html>

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

@ -0,0 +1,29 @@
/* 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";
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../../client/shared/test/shared-head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
this
);
const { DebuggerClient } = require("devtools/shared/client/debugger-client");
const { DebuggerServer } = require("devtools/server/debugger-server");
async function createLocalClient() {
// Instantiate a minimal server
DebuggerServer.init();
DebuggerServer.allowChromeProcess = true;
if (!DebuggerServer.createRootActor) {
DebuggerServer.registerAllActors();
}
const transport = DebuggerServer.connectPipe();
const client = new DebuggerClient(transport);
await client.connect();
return client;
}