Bug 997198 - Create a standalone reflow actor; r=bgrins

This commit is contained in:
Patrick Brosset 2014-05-12 16:51:06 +02:00
parent 8bdbedad0f
commit a1a0bf1ec7
8 changed files with 755 additions and 55 deletions

View File

@ -83,9 +83,7 @@ function*() {
is(editor.value, "20px", "Should have the right value in the editor."); is(editor.value, "20px", "Should have the right value in the editor.");
is(getStyle(node, "margin-left"), "20px", "Should have updated the margin."); is(getStyle(node, "margin-left"), "20px", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.") is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
is(span.textContent, 20, "Should have the right value in the box model."); is(span.textContent, 20, "Should have the right value in the box model.");
@ -139,7 +137,6 @@ function*() {
is(getStyle(node, "margin-right"), "", "Should have updated the margin."); is(getStyle(node, "margin-right"), "", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "margin-right"), "", "Should be the right margin-top on the element.") is(getStyle(node, "margin-right"), "", "Should be the right margin-top on the element.")
is(span.textContent, 10, "Should have the right value in the box model."); is(span.textContent, 10, "Should have the right value in the box model.");

View File

@ -48,7 +48,6 @@ function*() {
is(getStyle(node, "padding-bottom"), "7px", "Should have updated the padding"); is(getStyle(node, "padding-bottom"), "7px", "Should have updated the padding");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "padding-bottom"), "7px", "Should be the right padding.") is(getStyle(node, "padding-bottom"), "7px", "Should be the right padding.")
is(span.textContent, 7, "Should have the right value in the box model."); is(span.textContent, 7, "Should have the right value in the box model.");
@ -102,7 +101,6 @@ function*() {
is(getStyle(node, "padding-left"), "", "Should have updated the padding"); is(getStyle(node, "padding-left"), "", "Should have updated the padding");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "padding-left"), "", "Should be the right padding.") is(getStyle(node, "padding-left"), "", "Should be the right padding.")
is(span.textContent, 3, "Should have the right value in the box model."); is(span.textContent, 3, "Should have the right value in the box model.");

View File

@ -54,7 +54,6 @@ function*() {
is(getStyle(node, "padding-top"), "1em", "Should have updated the padding."); is(getStyle(node, "padding-top"), "1em", "Should have updated the padding.");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "padding-top"), "1em", "Should be the right padding.") is(getStyle(node, "padding-top"), "1em", "Should be the right padding.")
is(span.textContent, 16, "Should have the right value in the box model."); is(span.textContent, 16, "Should have the right value in the box model.");
@ -81,7 +80,6 @@ function*() {
is(getStyle(node, "border-bottom-width"), "0px", "Should have updated the border."); is(getStyle(node, "border-bottom-width"), "0px", "Should have updated the border.");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "border-bottom-width"), "0px", "Should be the right border-bottom-width.") is(getStyle(node, "border-bottom-width"), "0px", "Should be the right border-bottom-width.")
is(span.textContent, 0, "Should have the right value in the box model."); is(span.textContent, 0, "Should have the right value in the box model.");
@ -102,7 +100,6 @@ function*() {
is(editor.value, "2em", "Should have the right value in the editor."); is(editor.value, "2em", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_RETURN", {}, view); EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
is(getStyle(node, "padding-right"), "", "Should be the right padding.") is(getStyle(node, "padding-right"), "", "Should be the right padding.")
is(span.textContent, 32, "Should have the right value in the box model."); is(span.textContent, 32, "Should have the right value in the box model.");

View File

@ -18,6 +18,7 @@ Cu.import("resource://gre/modules/devtools/Console.jsm");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor"); const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils"); const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
const {ReflowFront} = devtools.require("devtools/server/actors/layout");
const NUMERIC = /^-?[\d\.]+$/; const NUMERIC = /^-?[\d\.]+$/;
const LONG_TEXT_ROTATE_LIMIT = 3; const LONG_TEXT_ROTATE_LIMIT = 3;
@ -90,13 +91,16 @@ EditingSession.prototype = {
let modifications = this._rules[0].startModifyingProperties(); let modifications = this._rules[0].startModifyingProperties();
for (let property of properties) { for (let property of properties) {
if (!this._modifications.has(property.name)) if (!this._modifications.has(property.name)) {
this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name)); this._modifications.set(property.name,
this.getPropertyFromRule(this._rules[0], property.name));
}
if (property.value == "") if (property.value == "") {
modifications.removeProperty(property.name); modifications.removeProperty(property.name);
else } else {
modifications.setProperty(property.name, property.value, ""); modifications.setProperty(property.name, property.value, "");
}
} }
return modifications.apply().then(null, console.error); return modifications.apply().then(null, console.error);
@ -110,26 +114,33 @@ EditingSession.prototype = {
let modifications = this._rules[0].startModifyingProperties(); let modifications = this._rules[0].startModifyingProperties();
for (let [property, value] of this._modifications) { for (let [property, value] of this._modifications) {
if (value != "") if (value != "") {
modifications.setProperty(property, value, ""); modifications.setProperty(property, value, "");
else } else {
modifications.removeProperty(property); modifications.removeProperty(property);
}
} }
return modifications.apply().then(null, console.error); return modifications.apply().then(null, console.error);
},
destroy: function() {
this._doc = null;
this._rules = null;
this._modifications.clear();
} }
}; };
function LayoutView(aInspector, aWindow) /**
{ * The layout-view panel
this.inspector = aInspector; * @param {InspectorPanel} inspector An instance of the inspector-panel
* currently loaded in the toolbox
* @param {Window} win The window containing the panel
*/
function LayoutView(inspector, win) {
this.inspector = inspector;
// <browser> is not always available (for Chrome targets for example) this.doc = win.document;
if (this.inspector.target.tab) {
this.browser = aInspector.target.tab.linkedBrowser;
}
this.doc = aWindow.document;
this.sizeLabel = this.doc.querySelector(".size > span"); this.sizeLabel = this.doc.querySelector(".size > span");
this.sizeHeadingLabel = this.doc.getElementById("element-size"); this.sizeHeadingLabel = this.doc.getElementById("element-size");
@ -137,13 +148,18 @@ function LayoutView(aInspector, aWindow)
} }
LayoutView.prototype = { LayoutView.prototype = {
init: function LV_init() { init: function() {
this.update = this.update.bind(this); this.update = this.update.bind(this);
this.onNewNode = this.onNewNode.bind(this);
this.onNewSelection = this.onNewSelection.bind(this); this.onNewSelection = this.onNewSelection.bind(this);
this.inspector.selection.on("new-node-front", this.onNewSelection); this.inspector.selection.on("new-node-front", this.onNewSelection);
this.onNewNode = this.onNewNode.bind(this);
this.inspector.sidebar.on("layoutview-selected", this.onNewNode); this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
this.inspector.sidebar.on("select", this.onSidebarSelect);
// Store for the different dimensions of the node. // Store for the different dimensions of the node.
// 'selector' refers to the element that holds the value in view.xhtml; // 'selector' refers to the element that holds the value in view.xhtml;
// 'property' is what we are measuring; // 'property' is what we are measuring;
@ -206,7 +222,9 @@ LayoutView.prototype = {
continue; continue;
let dimension = this.map[i]; let dimension = this.map[i];
editableItem({ element: this.doc.querySelector(dimension.selector) }, (element, event) => { editableItem({
element: this.doc.querySelector(dimension.selector)
}, (element, event) => {
this.initEditor(element, event, dimension); this.initEditor(element, event, dimension);
}); });
} }
@ -214,10 +232,39 @@ LayoutView.prototype = {
this.onNewNode(); this.onNewNode();
}, },
/**
* Start listening to reflows in the current tab.
*/
trackReflows: function() {
if (!this.reflowFront) {
let toolbox = this.inspector.toolbox;
if (toolbox.target.form.reflowActor) {
this.reflowFront = ReflowFront(toolbox.target.client, toolbox.target.form);
} else {
return;
}
}
this.reflowFront.on("reflows", this.update);
this.reflowFront.start();
},
/**
* Stop listening to reflows in the current tab.
*/
untrackReflows: function() {
if (!this.reflowFront) {
return;
}
this.reflowFront.off("reflows", this.update);
this.reflowFront.stop();
},
/** /**
* Called when the user clicks on one of the editable values in the layoutview * Called when the user clicks on one of the editable values in the layoutview
*/ */
initEditor: function LV_initEditor(element, event, dimension) { initEditor: function(element, event, dimension) {
let { property, realProperty } = dimension; let { property, realProperty } = dimension;
if (!realProperty) if (!realProperty)
realProperty = property; realProperty = property;
@ -233,17 +280,20 @@ LayoutView.prototype = {
}, },
change: (value) => { change: (value) => {
if (NUMERIC.test(value)) if (NUMERIC.test(value)) {
value += "px"; value += "px";
}
let properties = [ let properties = [
{ name: property, value: value } { name: property, value: value }
] ];
if (property.substring(0, 7) == "border-") { if (property.substring(0, 7) == "border-") {
let bprop = property.substring(0, property.length - 5) + "style"; let bprop = property.substring(0, property.length - 5) + "style";
let style = session.getProperty(bprop); let style = session.getProperty(bprop);
if (!style || style == "none" || style == "hidden") if (!style || style == "none" || style == "hidden") {
properties.push({ name: bprop, value: "solid" }); properties.push({ name: bprop, value: "solid" });
}
} }
session.setProperties(properties); session.setProperties(properties);
@ -251,8 +301,10 @@ LayoutView.prototype = {
done: (value, commit) => { done: (value, commit) => {
editor.elt.parentNode.classList.remove("editing"); editor.elt.parentNode.classList.remove("editing");
if (!commit) if (!commit) {
session.revert(); session.revert();
session.destroy();
}
} }
}, event); }, event);
}, },
@ -260,23 +312,35 @@ LayoutView.prototype = {
/** /**
* Is the layoutview visible in the sidebar? * Is the layoutview visible in the sidebar?
*/ */
isActive: function LV_isActive() { isActive: function() {
return this.inspector.sidebar.getCurrentTabID() == "layoutview"; return this.inspector &&
this.inspector.sidebar.getCurrentTabID() == "layoutview";
}, },
/** /**
* Destroy the nodes. Remove listeners. * Destroy the nodes. Remove listeners.
*/ */
destroy: function LV_destroy() { destroy: function() {
this.inspector.sidebar.off("layoutview-selected", this.onNewNode); this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
this.inspector.selection.off("new-node-front", this.onNewSelection); this.inspector.selection.off("new-node-front", this.onNewSelection);
if (this.browser) { this.inspector.sidebar.off("select", this.onSidebarSelect);
this.browser.removeEventListener("MozAfterPaint", this.update, true);
}
this.sizeHeadingLabel = null; this.sizeHeadingLabel = null;
this.sizeLabel = null; this.sizeLabel = null;
this.inspector = null; this.inspector = null;
this.doc = null; this.doc = null;
if (this.reflowFront) {
this.untrackReflows();
this.reflowFront.destroy();
this.reflowFront = null;
}
},
onSidebarSelect: function(e, sidebar) {
if (sidebar !== "layoutview") {
this.dim();
}
}, },
/** /**
@ -287,7 +351,10 @@ LayoutView.prototype = {
this.onNewNode().then(done, (err) => { console.error(err); done() }); this.onNewNode().then(done, (err) => { console.error(err); done() });
}, },
onNewNode: function LV_onNewNode() { /**
* @return a promise that resolves when the view has been updated
*/
onNewNode: function() {
if (this.isActive() && if (this.isActive() &&
this.inspector.selection.isConnected() && this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode()) { this.inspector.selection.isElementNode()) {
@ -295,41 +362,36 @@ LayoutView.prototype = {
} else { } else {
this.dim(); this.dim();
} }
return this.update(); return this.update();
}, },
/** /**
* Hide the layout boxes. No node are selected. * Hide the layout boxes and stop refreshing on reflows. No node is selected
* or the layout-view sidebar is inactive.
*/ */
dim: function LV_dim() { dim: function() {
if (this.browser) { this.untrackReflows();
this.browser.removeEventListener("MozAfterPaint", this.update, true);
}
this.trackingPaint = false;
this.doc.body.classList.add("dim"); this.doc.body.classList.add("dim");
this.dimmed = true; this.dimmed = true;
}, },
/** /**
* Show the layout boxes. A node is selected. * Show the layout boxes and start refreshing on reflows. A node is selected
* and the layout-view side is active.
*/ */
undim: function LV_undim() { undim: function() {
if (!this.trackingPaint) { this.trackReflows();
if (this.browser) {
this.browser.addEventListener("MozAfterPaint", this.update, true);
}
this.trackingPaint = true;
}
this.doc.body.classList.remove("dim"); this.doc.body.classList.remove("dim");
this.dimmed = false; this.dimmed = false;
}, },
/** /**
* Compute the dimensions of the node and update the values in * Compute the dimensions of the node and update the values in
* the layoutview/view.xhtml document. Returns a promise that will be resolved * the layoutview/view.xhtml document.
* when complete. * @return a promise that will be resolved when complete.
*/ */
update: function LV_update() { update: function() {
let lastRequest = Task.spawn((function*() { let lastRequest = Task.spawn((function*() {
if (!this.isActive() || if (!this.isActive() ||
!this.inspector.selection.isConnected() || !this.inspector.selection.isConnected() ||
@ -415,6 +477,10 @@ LayoutView.prototype = {
return this._lastRequest = lastRequest; return this._lastRequest = lastRequest;
}, },
/**
* Show the box-model highlighter on the currently selected element
* @param {Object} options Options passed to the highlighter actor
*/
showBoxModel: function(options={}) { showBoxModel: function(options={}) {
let toolbox = this.inspector.toolbox; let toolbox = this.inspector.toolbox;
let nodeFront = this.inspector.selection.nodeFront; let nodeFront = this.inspector.selection.nodeFront;
@ -422,6 +488,9 @@ LayoutView.prototype = {
toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
}, },
/**
* Hide the box-model highlighter on the currently selected element
*/
hideBoxModel: function() { hideBoxModel: function() {
let toolbox = this.inspector.toolbox; let toolbox = this.inspector.toolbox;

View File

@ -0,0 +1,394 @@
/* 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";
/**
* About the types of objects in this file:
*
* - ReflowActor: the actor class used for protocol purposes.
* Mostly empty, just gets an instance of LayoutChangesObserver and forwards
* its "reflows" events to clients.
*
* - Observable: A utility parent class, meant at being extended by classes that
* need a start/stop behavior.
*
* - LayoutChangesObserver: extends Observable and uses the ReflowObserver, to
* track reflows on the page.
* Used by the LayoutActor, but is also exported on the module, so can be used
* by any other actor that needs it.
*
* - Dedicated observers: There's only one of them for now: ReflowObserver which
* listens to reflow events via the docshell,
* These dedicated classes are used by the LayoutChangesObserver.
*/
const {Ci, Cu} = require("chrome");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const protocol = require("devtools/server/protocol");
const {method, Arg, RetVal, types} = protocol;
const events = require("sdk/event/core");
const Heritage = require("sdk/core/heritage");
const EventEmitter = require("devtools/toolkit/event-emitter");
exports.register = function(handle) {
handle.addGlobalActor(ReflowActor, "reflowActor");
handle.addTabActor(ReflowActor, "reflowActor");
};
exports.unregister = function(handle) {
handle.removeGlobalActor(ReflowActor);
handle.removeTabActor(ReflowActor);
};
/**
* The reflow actor tracks reflows and emits events about them.
*/
let ReflowActor = protocol.ActorClass({
typeName: "reflow",
events: {
/**
* The reflows event is emitted when reflows have been detected. The event
* is sent with an array of reflows that occured. Each item has the
* following properties:
* - start {Number}
* - end {Number}
* - isInterruptible {Boolean}
*/
"reflows" : {
type: "reflows",
reflows: Arg(0, "array:json")
}
},
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onReflow = this._onReflow.bind(this);
this.observer = getLayoutChangesObserver(tabActor);
this._isStarted = false;
},
/**
* The reflow actor is the first (and last) in its hierarchy to use protocol.js
* so it doesn't have a parent protocol actor that takes care of its lifetime.
* So it needs a disconnect method to cleanup.
*/
disconnect: function() {
this.destroy();
},
destroy: function() {
this.stop();
releaseLayoutChangesObserver(this.tabActor);
this.observer = null;
this.tabActor = null;
protocol.Actor.prototype.destroy.call(this);
},
/**
* Start tracking reflows and sending events to clients about them.
* This is a oneway method, do not expect a response and it won't return a
* promise.
*/
start: method(function() {
if (!this._isStarted) {
this.observer.on("reflows", this._onReflow);
this._isStarted = true;
}
}, {oneway: true}),
/**
* Stop tracking reflows and sending events to clients about them.
* This is a oneway method, do not expect a response and it won't return a
* promise.
*/
stop: method(function() {
if (this._isStarted) {
this.observer.off("reflows", this._onReflow);
this._isStarted = false;
}
}, {oneway: true}),
_onReflow: function(event, reflows) {
if (this._isStarted) {
events.emit(this, "reflows", reflows);
}
}
});
/**
* Usage example of the reflow front:
*
* let front = ReflowFront(toolbox.target.client, toolbox.target.form);
* front.on("reflows", this._onReflows);
* front.start();
* // now wait for events to come
*/
exports.ReflowFront = protocol.FrontClass(ReflowActor, {
initialize: function(client, {reflowActor}) {
protocol.Front.prototype.initialize.call(this, client, {actor: reflowActor});
client.addActorPool(this);
this.manage(this);
},
destroy: function() {
protocol.Front.prototype.destroy.call(this);
},
});
/**
* Base class for all sorts of observers we need to create for a given window.
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime the observer observes something
*/
function Observable(tabActor, callback) {
this.tabActor = tabActor;
this.win = tabActor.window;
this.callback = callback;
}
Observable.prototype = {
/**
* Is the observer currently observing
*/
observing: false,
/**
* Start observing whatever it is this observer is supposed to observe
*/
start: function() {
if (!this.observing) {
this._start();
this.observing = true;
}
},
_start: function() {
/* To be implemented by sub-classes */
},
/**
* Stop observing
*/
stop: function() {
if (this.observing) {
this._stop();
this.observing = false;
}
},
_stop: function() {
/* To be implemented by sub-classes */
},
/**
* To be called by sub-classes when something has been observed
*/
notifyCallback: function(...args) {
this.observing && this.callback && this.callback.apply(null, args);
},
/**
* Stop observing and detroy this observer instance
*/
destroy: function() {
this.stop();
this.callback = null;
this.win = null;
this.tabActor = null;
}
};
/**
* The LayoutChangesObserver class is instantiated only once per given tab
* and is used to track reflows and dom and style changes in that tab.
* The LayoutActor uses this class to send reflow events to its clients.
*
* This class isn't exported on the module because it shouldn't be instantiated
* to avoid creating several instances per tabs.
* Use `getLayoutChangesObserver(tabActor)`
* and `releaseLayoutChangesObserver(tabActor)`
* which are exported to get and release instances.
*
* The observer loops every EVENT_BATCHING_DELAY ms and checks if layout changes
* have happened since the last loop iteration. If there are, it sends the
* corresponding events:
*
* - "reflows", with an array of all the reflows that occured,
*
* @param {TabActor} tabActor
*/
function LayoutChangesObserver(tabActor) {
Observable.call(this, tabActor);
this._startEventLoop = this._startEventLoop.bind(this);
// Creating the various observers we're going to need
// For now, just the reflow observer, but later we can add markupMutation,
// styleSheetChanges and styleRuleChanges
this._onReflow = this._onReflow.bind(this);
this.reflowObserver = new ReflowObserver(this.tabActor, this._onReflow);
EventEmitter.decorate(this);
}
LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
/**
* How long does this observer waits before emitting a batched reflows event.
* The lower the value, the more event packets will be sent to clients,
* potentially impacting performance.
* The higher the value, the more time we'll wait, this is better for
* performance but has an effect on how soon changes are shown in the toolbox.
*/
EVENT_BATCHING_DELAY: 300,
/**
* Destroying this instance of LayoutChangesObserver will stop the batched
* events from being sent.
*/
destroy: function() {
this.reflowObserver.destroy();
this.reflows = null;
Observable.prototype.destroy.call(this);
},
_start: function() {
this.reflows = [];
this._startEventLoop();
this.reflowObserver.start();
},
_stop: function() {
this._stopEventLoop();
this.reflows = [];
this.reflowObserver.stop();
},
/**
* Start the event loop, which regularly checks if there are any observer
* events to be sent as batched events
* Calls itself in a loop.
*/
_startEventLoop: function() {
// Send any reflows we have
if (this.reflows && this.reflows.length) {
this.emit("reflows", this.reflows);
this.reflows = [];
}
this.eventLoopTimer = this.win.setTimeout(this._startEventLoop,
this.EVENT_BATCHING_DELAY);
},
_stopEventLoop: function() {
this.win.clearTimeout(this.eventLoopTimer);
},
/**
* Executed whenever a reflow is observed. Only stacks the reflow in the
* reflows array.
* The EVENT_BATCHING_DELAY loop will take care of it later.
* @param {Number} start When the reflow started
* @param {Number} end When the reflow ended
* @param {Boolean} isInterruptible
*/
_onReflow: function(start, end, isInterruptible) {
// XXX: when/if bug 997092 gets fixed, we will be able to know which
// elements have been reflowed, which would be a nice thing to add here.
this.reflows.push({
start: start,
end: end,
isInterruptible: isInterruptible
});
}
});
/**
* Get a LayoutChangesObserver instance for a given window. This function makes
* sure there is only one instance per window.
* @param {TabActor} tabActor
* @return {LayoutChangesObserver}
*/
let observedWindows = new Map();
function getLayoutChangesObserver(tabActor) {
let observerData = observedWindows.get(tabActor);
if (observerData) {
observerData.refCounting ++;
return observerData.observer;
}
let obs = new LayoutChangesObserver(tabActor);
observedWindows.set(tabActor, {
observer: obs,
refCounting: 1 // counting references allows to stop the observer when no
// tabActor owns an instance
});
obs.start();
return obs;
};
exports.getLayoutChangesObserver = getLayoutChangesObserver;
/**
* Release a LayoutChangesObserver instance that was retrieved by
* getLayoutChangesObserver. This is required to ensure the tabActor reference
* is removed and the observer is eventually stopped and destroyed.
* @param {TabActor} tabActor
*/
function releaseLayoutChangesObserver(tabActor) {
let observerData = observedWindows.get(tabActor);
if (!observerData) {
return;
}
observerData.refCounting --;
if (!observerData.refCounting) {
observerData.observer.destroy();
observedWindows.delete(tabActor);
}
};
exports.releaseLayoutChangesObserver = releaseLayoutChangesObserver;
/**
* Instantiate and start a reflow observer on a given window's document element.
* Will report any reflow that occurs in this window's docshell.
* @extends Observable
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime a reflow occurs
*/
function ReflowObserver(tabActor, callback) {
Observable.call(this, tabActor, callback);
this.docshell = this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
}
ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
Ci.nsISupportsWeakReference]),
_start: function() {
this.docshell.addWeakReflowObserver(this);
},
_stop: function() {
this.docshell.removeWeakReflowObserver(this);
},
reflow: function(start, end) {
this.notifyCallback(start, end, false);
},
reflowInterruptible: function(start, end) {
this.notifyCallback(start, end, true);
},
destroy: function() {
Observable.prototype.destroy.call(this);
this.docshell = null;
}
});

View File

@ -391,6 +391,7 @@ var DebuggerServer = {
this.registerModule("devtools/server/actors/tracer"); this.registerModule("devtools/server/actors/tracer");
this.registerModule("devtools/server/actors/memory"); this.registerModule("devtools/server/actors/memory");
this.registerModule("devtools/server/actors/eventlooplag"); this.registerModule("devtools/server/actors/eventlooplag");
this.registerModule("devtools/server/actors/layout");
if ("nsIProfiler" in Ci) { if ("nsIProfiler" in Ci) {
this.addActors("resource://gre/modules/devtools/server/actors/profiler.js"); this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
} }

View File

@ -0,0 +1,243 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test the LayoutChangesObserver
let {
getLayoutChangesObserver,
releaseLayoutChangesObserver
} = devtools.require("devtools/server/actors/layout");
// Mock the tabActor since we only really want to test the LayoutChangesObserver
// and don't want to depend on a window object, nor want to test protocol.js
function MockTabActor() {
this.window = new MockWindow();
}
function MockWindow() {}
MockWindow.prototype = {
QueryInterface: function() {
let self = this;
return {
getInterface: function() {
return {
QueryInterface: function() {
self.docShell = new MockDocShell();
return self.docShell;
}
};
}
};
},
setTimeout: function(cb) {
// Simply return the cb itself so that we can execute it in the test instead
// of depending on a real timeout
return cb;
},
clearTimeout: function() {}
};
function MockDocShell() {
this.observer = null;
}
MockDocShell.prototype = {
addWeakReflowObserver: function(observer) {
this.observer = observer;
},
removeWeakReflowObserver: function(observer) {}
};
function run_test() {
instancesOfObserversAreSharedBetweenWindows();
eventsAreBatched();
noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts();
observerIsAlreadyStarted();
destroyStopsObserving();
stoppingAndStartingSeveralTimesWorksCorrectly();
reflowsArentStackedWhenStopped();
stackedReflowsAreResetOnStop();
}
function instancesOfObserversAreSharedBetweenWindows() {
do_print("Checking that when requesting twice an instances of the observer " +
"for the same TabActor, the instance is shared");
do_print("Checking 2 instances of the observer for the tabActor 1");
let tabActor1 = new MockTabActor();
let obs11 = getLayoutChangesObserver(tabActor1);
let obs12 = getLayoutChangesObserver(tabActor1);
do_check_eq(obs11, obs12);
do_print("Checking 2 instances of the observer for the tabActor 2");
let tabActor2 = new MockTabActor();
let obs21 = getLayoutChangesObserver(tabActor2);
let obs22 = getLayoutChangesObserver(tabActor2);
do_check_eq(obs21, obs22);
do_print("Checking that observers instances for 2 different tabActors are " +
"different");
do_check_neq(obs11, obs21);
releaseLayoutChangesObserver(tabActor1);
releaseLayoutChangesObserver(tabActor1);
releaseLayoutChangesObserver(tabActor2);
releaseLayoutChangesObserver(tabActor2);
}
function eventsAreBatched() {
do_print("Checking that reflow events are batched and only sent when the " +
"timeout expires");
// Note that in this test, we mock the TabActor and its window property, so we
// also mock the setTimeout/clearTimeout mechanism and just call the callback
// manually
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
let reflowsEvents = [];
let onReflows = (event, reflows) => reflowsEvents.push(reflows);
observer.on("reflows", onReflows);
do_print("Fake one reflow event");
tabActor.window.docShell.observer.reflow();
do_print("Checking that no batched reflow event has been emitted");
do_check_eq(reflowsEvents.length, 0);
do_print("Fake another reflow event");
tabActor.window.docShell.observer.reflow();
do_print("Checking that still no batched reflow event has been emitted");
do_check_eq(reflowsEvents.length, 0);
do_print("Faking timeout expiration and checking that reflow events are sent");
observer.eventLoopTimer();
do_check_eq(reflowsEvents.length, 1);
do_check_eq(reflowsEvents[0].length, 2);
observer.off("reflows", onReflows);
releaseLayoutChangesObserver(tabActor);
}
function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() {
do_print("Checking that if no reflows were detected and the event batching " +
"loop expires, then no reflows event is sent");
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
let reflowsEvents = [];
let onReflows = (event, reflows) => reflowsEvents.push(reflows);
observer.on("reflows", onReflows);
do_print("Faking timeout expiration and checking for reflows");
observer.eventLoopTimer();
do_check_eq(reflowsEvents.length, 0);
observer.off("reflows", onReflows);
releaseLayoutChangesObserver(tabActor);
}
function observerIsAlreadyStarted() {
do_print("Checking that the observer is already started when getting it");
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_check_true(observer.observing);
observer.stop();
do_check_false(observer.observing);
observer.start();
do_check_true(observer.observing);
releaseLayoutChangesObserver(tabActor);
}
function destroyStopsObserving() {
do_print("Checking that the destroying the observer stops it");
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_check_true(observer.observing);
observer.destroy();
do_check_false(observer.observing);
releaseLayoutChangesObserver(tabActor);
}
function stoppingAndStartingSeveralTimesWorksCorrectly() {
do_print("Checking that the stopping and starting several times the observer" +
" works correctly");
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_check_true(observer.observing);
observer.start();
observer.start();
observer.start();
do_check_true(observer.observing);
observer.stop();
do_check_false(observer.observing);
observer.stop();
observer.stop();
do_check_false(observer.observing);
releaseLayoutChangesObserver(tabActor);
}
function reflowsArentStackedWhenStopped() {
do_print("Checking that when stopped, reflows aren't stacked in the observer");
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_print("Stoping the observer");
observer.stop();
do_print("Faking reflows");
tabActor.window.docShell.observer.reflow();
tabActor.window.docShell.observer.reflow();
tabActor.window.docShell.observer.reflow();
do_print("Checking that reflows aren't recorded");
do_check_eq(observer.reflows.length, 0);
do_print("Starting the observer and faking more reflows");
observer.start();
tabActor.window.docShell.observer.reflow();
tabActor.window.docShell.observer.reflow();
tabActor.window.docShell.observer.reflow();
do_print("Checking that reflows are recorded");
do_check_eq(observer.reflows.length, 3);
releaseLayoutChangesObserver(tabActor);
}
function stackedReflowsAreResetOnStop() {
do_print("Checking that stacked reflows are reset on stop");
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
tabActor.window.docShell.observer.reflow();
do_check_eq(observer.reflows.length, 1);
observer.stop();
do_check_eq(observer.reflows.length, 0);
tabActor.window.docShell.observer.reflow();
do_check_eq(observer.reflows.length, 0);
observer.start();
do_check_eq(observer.reflows.length, 0);
tabActor.window.docShell.observer.reflow();
do_check_eq(observer.reflows.length, 1);
releaseLayoutChangesObserver(tabActor);
}

View File

@ -198,5 +198,6 @@ reason = bug 820380
[test_ignore_caught_exceptions.js] [test_ignore_caught_exceptions.js]
[test_requestTypes.js] [test_requestTypes.js]
reason = bug 937197 reason = bug 937197
[test_layout-reflows-observer.js]
[test_protocolSpec.js] [test_protocolSpec.js]