Backed out 2 changesets (bug 1943998) for causing wd failures @ phases.py CLOSED...
[gecko.git] / devtools / client / fronts / walker.js
blob0cd063bd6426fa687d76355018b2e8cc258baf87
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const {
8 FrontClassWithSpec,
9 types,
10 registerFront,
11 } = require("resource://devtools/shared/protocol.js");
12 const { walkerSpec } = require("resource://devtools/shared/specs/walker.js");
13 const {
14 safeAsyncMethod,
15 } = require("resource://devtools/shared/async-utils.js");
17 /**
18 * Client side of the DOM walker.
20 class WalkerFront extends FrontClassWithSpec(walkerSpec) {
21 constructor(client, targetFront, parentFront) {
22 super(client, targetFront, parentFront);
23 this._isPicking = false;
24 this._orphaned = new Set();
25 this._retainedOrphans = new Set();
27 // Set to true if cleanup should be requested after every mutation list.
28 this.autoCleanup = true;
30 this._rootNodePromise = new Promise(
31 r => (this._rootNodePromiseResolve = r)
34 this._onRootNodeAvailable = this._onRootNodeAvailable.bind(this);
35 this._onRootNodeDestroyed = this._onRootNodeDestroyed.bind(this);
37 // pick/cancelPick requests can be triggered while the Walker is being destroyed.
38 this.pick = safeAsyncMethod(this.pick.bind(this), () => this.isDestroyed());
39 this.cancelPick = safeAsyncMethod(this.cancelPick.bind(this), () =>
40 this.isDestroyed()
43 this.before("new-mutations", this.onMutations.bind(this));
45 // Those events will be emitted if watchRootNode was called on the
46 // corresponding WalkerActor, which should be handled by the ResourceCommand
47 // as long as a consumer is watching for root-node resources.
48 // This should be fixed by using watchResources directly from the walker
49 // front, either with the ROOT_NODE resource, or with the DOCUMENT_EVENT
50 // resource. See Bug 1663973.
51 this.on("root-available", this._onRootNodeAvailable);
52 this.on("root-destroyed", this._onRootNodeDestroyed);
55 // Update the object given a form representation off the wire.
56 form(json) {
57 this.actorID = json.actor;
59 // The rootNode property should usually be provided via watchRootNode.
60 // However tests are currently using the walker front without explicitly
61 // calling watchRootNode, so we keep this assignment as a fallback.
62 this.rootNode = types.getType("domnode").read(json.root, this);
64 // Bug 1861328: boolean set to true when color scheme can't be changed (happens when `privacy.resistFingerprinting` is set to true)
65 this.rfpCSSColorScheme = json.rfpCSSColorScheme;
67 this.traits = json.traits;
70 /**
71 * Clients can use walker.rootNode to get the current root node of the
72 * walker, but during a reload the root node might be null. This
73 * method returns a promise that will resolve to the root node when it is
74 * set.
76 async getRootNode() {
77 let rootNode = this.rootNode;
78 if (!rootNode) {
79 rootNode = await this._rootNodePromise;
82 return rootNode;
85 /**
86 * When reading an actor form off the wire, we want to hook it up to its
87 * parent or host front. The protocol guarantees that the parent will
88 * be seen by the client in either a previous or the current request.
89 * So if we've already seen this parent return it, otherwise create
90 * a bare-bones stand-in node. The stand-in node will be updated
91 * with a real form by the end of the deserialization.
93 ensureDOMNodeFront(id) {
94 const front = this.getActorByID(id);
95 if (front) {
96 return front;
99 return types.getType("domnode").read({ actor: id }, this, "standin");
103 * See the documentation for WalkerActor.prototype.retainNode for
104 * information on retained nodes.
106 * From the client's perspective, `retainNode` can fail if the node in
107 * question is removed from the ownership tree before the `retainNode`
108 * request reaches the server. This can only happen if the client has
109 * asked the server to release nodes but hasn't gotten a response
110 * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
111 * set is outstanding.
113 * If either of those requests is outstanding AND releases the retained
114 * node, this request will fail with noSuchActor, but the ownership tree
115 * will stay in a consistent state.
117 * Because the protocol guarantees that requests will be processed and
118 * responses received in the order they were sent, we get the right
119 * semantics by setting our local retained flag on the node only AFTER
120 * a SUCCESSFUL retainNode call.
122 async retainNode(node) {
123 await super.retainNode(node);
124 node.retained = true;
127 async unretainNode(node) {
128 await super.unretainNode(node);
129 node.retained = false;
130 if (this._retainedOrphans.has(node)) {
131 this._retainedOrphans.delete(node);
132 this._releaseFront(node);
136 releaseNode(node, options = {}) {
137 // NodeFront.destroy will destroy children in the ownership tree too,
138 // mimicking what the server will do here.
139 const actorID = node.actorID;
140 this._releaseFront(node, !!options.force);
141 return super.releaseNode({ actorID });
144 async findInspectingNode() {
145 const response = await super.findInspectingNode();
146 return response.node;
149 async querySelector(queryNode, selector) {
150 const response = await super.querySelector(queryNode, selector);
151 return response.node;
154 async getIdrefNode(queryNode, id) {
155 const response = await super.getIdrefNode(queryNode, id);
156 return response.node;
159 async getNodeActorFromWindowID(windowID) {
160 const response = await super.getNodeActorFromWindowID(windowID);
161 return response ? response.node : null;
164 async getNodeActorFromContentDomReference(contentDomReference) {
165 const response = await super.getNodeActorFromContentDomReference(
166 contentDomReference
168 return response ? response.node : null;
171 async getStyleSheetOwnerNode(styleSheetActorID) {
172 const response = await super.getStyleSheetOwnerNode(styleSheetActorID);
173 return response ? response.node : null;
176 async getNodeFromActor(actorID, path) {
177 const response = await super.getNodeFromActor(actorID, path);
178 return response ? response.node : null;
181 _releaseFront(node, force) {
182 if (node.retained && !force) {
183 node.reparent(null);
184 this._retainedOrphans.add(node);
185 return;
188 if (node.retained) {
189 // Forcing a removal.
190 this._retainedOrphans.delete(node);
193 // Release any children
194 for (const child of node.treeChildren()) {
195 this._releaseFront(child, force);
198 // All children will have been removed from the node by this point.
199 node.reparent(null);
200 node.destroy();
204 * Get any unprocessed mutation records and process them.
206 // eslint-disable-next-line complexity
207 async getMutations(options = {}) {
208 const mutations = await super.getMutations(options);
209 const emitMutations = [];
210 for (const change of mutations) {
211 // The target is only an actorID, get the associated front.
212 const targetID = change.target;
213 const targetFront = this.getActorByID(targetID);
215 if (!targetFront) {
216 console.warn(
217 "Got a mutation for an unexpected actor: " +
218 targetID +
219 ", please file a bug on bugzilla.mozilla.org!"
221 console.trace();
222 continue;
225 const emittedMutation = Object.assign(change, { target: targetFront });
227 if (change.type === "childList") {
228 // Update the ownership tree according to the mutation record.
229 const addedFronts = [];
230 const removedFronts = [];
231 for (const removed of change.removed) {
232 const removedFront = this.getActorByID(removed);
233 if (!removedFront) {
234 console.error(
235 "Got a removal of an actor we didn't know about: " + removed
237 continue;
239 // Remove from the ownership tree
240 removedFront.reparent(null);
242 // This node is orphaned unless we get it in the 'added' list
243 // eventually.
244 this._orphaned.add(removedFront);
245 removedFronts.push(removedFront);
247 for (const added of change.added) {
248 const addedFront = this.getActorByID(added);
249 if (!addedFront) {
250 console.error(
251 "Got an addition of an actor we didn't know " + "about: " + added
253 continue;
255 addedFront.reparent(targetFront);
257 // The actor is reconnected to the ownership tree, unorphan
258 // it.
259 this._orphaned.delete(addedFront);
260 addedFronts.push(addedFront);
263 // Before passing to users, replace the added and removed actor
264 // ids with front in the mutation record.
265 emittedMutation.added = addedFronts;
266 emittedMutation.removed = removedFronts;
268 // If this is coming from a DOM mutation, the actor's numChildren
269 // was passed in. Otherwise, it is simulated from a frame load or
270 // unload, so don't change the front's form.
271 if ("numChildren" in change) {
272 targetFront._form.numChildren = change.numChildren;
274 } else if (change.type === "shadowRootAttached") {
275 targetFront._form.isShadowHost = true;
276 } else if (change.type === "customElementDefined") {
277 targetFront._form.customElementLocation = change.customElementLocation;
278 } else if (change.type === "unretained") {
279 // Retained orphans were force-released without the intervention of
280 // client (probably a navigated frame).
281 for (const released of change.nodes) {
282 const releasedFront = this.getActorByID(released);
283 this._retainedOrphans.delete(released);
284 this._releaseFront(releasedFront, true);
286 } else {
287 targetFront.updateMutation(change);
290 // Update the inlineTextChild property of the target for a selected list of
291 // mutation types.
292 if (
293 change.type === "inlineTextChild" ||
294 change.type === "childList" ||
295 change.type === "shadowRootAttached"
297 if (change.inlineTextChild) {
298 targetFront.inlineTextChild = types
299 .getType("domnode")
300 .read(change.inlineTextChild, this);
301 } else {
302 targetFront.inlineTextChild = undefined;
306 emitMutations.push(emittedMutation);
309 if (options.cleanup) {
310 for (const node of this._orphaned) {
311 // This will move retained nodes to this._retainedOrphans.
312 this._releaseFront(node);
314 this._orphaned = new Set();
317 this.emit("mutations", emitMutations);
321 * Handle the `new-mutations` notification by fetching the
322 * available mutation records.
324 onMutations() {
325 // Fetch and process the mutations.
326 this.getMutations({ cleanup: this.autoCleanup }).catch(() => {});
329 isLocal() {
330 return !!this.conn._transport._serverConnection;
333 async removeNode(node) {
334 const previousSibling = await this.previousSibling(node);
335 const nextSibling = await super.removeNode(node);
336 return {
337 previousSibling,
338 nextSibling,
342 async children(node, options) {
343 if (!node.useChildTargetToFetchChildren) {
344 return super.children(node, options);
346 const target = await node.connectToFrame();
348 // We had several issues in the past where `connectToFrame` was returning the same
349 // target as the owner document one, which led to the inspector being broken.
350 // Ultimately, we shouldn't get to this point (fix should happen in connectToFrame or
351 // on the server, e.g. for Bug 1752342), but at least this will serve as a safe guard
352 // so we don't freeze/crash the inspector.
353 if (
354 target == this.targetFront &&
355 Services.prefs.getBoolPref(
356 "devtools.testing.bypass-walker-children-iframe-guard",
357 false
358 ) !== true
360 console.warn("connectToFrame returned an unexpected target");
361 return {
362 nodes: [],
363 hasFirst: true,
364 hasLast: true,
368 const walker = (await target.getFront("inspector")).walker;
370 // Finally retrieve the NodeFront of the remote frame's document
371 const documentNode = await walker.getRootNode();
373 // Force reparenting through the remote frame boundary.
374 documentNode.reparent(node);
376 // And return the same kind of response `walker.children` returns
377 return {
378 nodes: [documentNode],
379 hasFirst: true,
380 hasLast: true,
385 * Ensure that the RootNode of this Walker has the right parent NodeFront.
387 * This method does nothing if we are on the top level target's WalkerFront,
388 * as the RootNode won't have any parent.
390 * Otherwise, if we are in an iframe's WalkerFront, we would expect the parent
391 * of the RootNode (i.e. the NodeFront for the document loaded within the iframe)
392 * to be the <iframe>'s NodeFront. Because of fission, the two NodeFront may refer
393 * to DOM Element running in distinct processes and so the NodeFront comes from
394 * two distinct Targets and two distinct WalkerFront.
395 * This is why we need this manual "reparent" code to do the glue between the
396 * two documents.
398 async reparentRemoteFrame() {
399 const parentTarget = await this.targetFront.getParentTarget();
400 if (!parentTarget) {
401 return;
403 // Don't reparent if we are on the top target
404 if (parentTarget == this.targetFront) {
405 return;
407 // Get the NodeFront for the embedder element
408 // i.e. the <iframe> element which is hosting the document that
409 const parentWalker = (await parentTarget.getFront("inspector")).walker;
410 // As this <iframe> most likely runs in another process, we have to get it through the parent
411 // target's WalkerFront.
412 const parentNode = (
413 await parentWalker.getEmbedderElement(this.targetFront.browsingContextID)
414 ).node;
416 // Finally, set this embedder element's node front as the
417 const documentNode = await this.getRootNode();
418 documentNode.reparent(parentNode);
421 _onRootNodeAvailable(rootNode) {
422 if (rootNode.isTopLevelDocument) {
423 this.rootNode = rootNode;
424 this._rootNodePromiseResolve(this.rootNode);
428 _onRootNodeDestroyed(rootNode) {
429 if (rootNode.isTopLevelDocument) {
430 this._rootNodePromise = new Promise(
431 r => (this._rootNodePromiseResolve = r)
433 this.rootNode = null;
438 * Start the element picker on the debuggee target.
439 * @param {Boolean} doFocus - Optionally focus the content area once the picker is
440 * activated.
442 pick(doFocus) {
443 if (this._isPicking) {
444 return Promise.resolve();
447 this._isPicking = true;
449 return super.pick(
450 doFocus,
451 this.targetFront.commands.descriptorFront.isLocalTab
456 * Stop the element picker.
458 cancelPick() {
459 if (!this._isPicking) {
460 return Promise.resolve();
463 this._isPicking = false;
464 return super.cancelPick();
468 exports.WalkerFront = WalkerFront;
469 registerFront(WalkerFront);