Bug 1222409 - Listen to window resize events on server and use this to refresh style-inspector; r=bgrins

1 - Make the LayoutChangesObserver also send "resize" events; r=bgrins
The LayoutChangesObserver was originally made to observe all kinds of
layout-related events. So far, it was only observing reflows though.
This adds the capability to also observe resize events on the content
window.

2 - Removed the non-e10s rule/computed-views refreshing mechanism; r=bgrins
When the window is resized, the styles shown in the rule-view and
computed-view need to be updated (media-queries may be at play).
This was done before using a local-only, non-e10s solution. The
inspector-panel would listen to the resize event on the linkedBrowser
in the current tab.
This, obviously, did not work with e10s or across a remote connection.
This change just removes all of the code involved with this.
This won't cause any regression or backwards-compatibility problems as
a new server-driven resize observer is being put in place in this bug.
Even if you connected to an older server, you wouldn't see a difference
because the refresh-on-resize didn't work over remote connections already.

3 - Refresh the style-inspector when the LayoutChangesObserver detects resize
The implementation is simple, the inspector actor uses the
LayoutChangesObserver to detect window resize, and when it does, it
forwards the event to its front.
This is similar to how we deal with reflow events, except that for
reflows, the inspector actor (walker in this case), first filters on
the server to see if the reflow would indeed impact known nodes.
For resize events, it seemed more complex to do this kind of server
side filtering as this would involve remembering which node is currently
selected and which style were applied, and then compare that with the
new styles.

4 - Tests for the style-inspector refresh on window resize
This commit is contained in:
Patrick Brosset 2015-11-26 12:18:17 +01:00
parent 13217575d0
commit 247a257a7a
12 changed files with 571 additions and 359 deletions

View File

@ -32,8 +32,6 @@ loader.lazyGetter(this, "clipboardHelper", () => {
loader.lazyImporter(this, "CommandUtils", "resource://devtools/client/shared/DeveloperToolbar.jsm");
const LAYOUT_CHANGE_TIMER = 250;
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the breadcrumbs, the markup view, and the sidebar
@ -49,8 +47,6 @@ const LAYOUT_CHANGE_TIMER = 250;
* view has been reloaded)
* - markuploaded
* Fired when the markup-view frame has loaded
* - layout-change
* Fired when the layout of the inspector changes
* - breadcrumbs-updated
* Fired when the breadcrumb widget updates to a new node
* - layoutview-updated
@ -90,7 +86,6 @@ function InspectorPanel(iframeWindow, toolbox) {
this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
this.onDetached = this.onDetached.bind(this);
this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
@ -170,9 +165,6 @@ InspectorPanel.prototype = {
this._toolbox.on("host-changed", this.onToolboxHostChanged);
if (this.target.isLocalTab) {
this.browser = this.target.tab.linkedBrowser;
this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
// Show a warning when the debugger is paused.
// We show the warning only when the inspector
// is selected.
@ -469,8 +461,6 @@ InspectorPanel.prototype = {
return;
}
this.cancelLayoutChange();
// Wait for all the known tools to finish updating and then let the
// client know.
let selection = this.selection.nodeFront;
@ -570,7 +560,6 @@ InspectorPanel.prototype = {
* node was selected).
*/
onDetached: function(event, parentNode) {
this.cancelLayoutChange();
this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached");
},
@ -589,12 +578,6 @@ InspectorPanel.prototype = {
}
this.cancelUpdate();
this.cancelLayoutChange();
if (this.browser) {
this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
this.browser = null;
}
this.target.off("will-navigate", this._onBeforeNavigate);
@ -1392,41 +1375,5 @@ InspectorPanel.prototype = {
this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
clipboardHelper.copyString(url);
}, console.error);
},
/**
* Trigger a high-priority layout change for things that need to be
* updated immediately
*/
immediateLayoutChange: function() {
this.emit("layout-change");
},
/**
* Schedule a low-priority change event for things like paint
* and resize.
*/
scheduleLayoutChange: function(event) {
// Filter out non browser window resize events (i.e. triggered by iframes)
if (this.browser.contentWindow === event.target) {
if (this._timer) {
return null;
}
this._timer = this.panelWin.setTimeout(() => {
this.emit("layout-change");
this._timer = null;
}, LAYOUT_CHANGE_TIMER);
}
},
/**
* Cancel a pending low-priority change event if any is
* scheduled.
*/
cancelLayoutChange: function() {
if (this._timer) {
this.panelWin.clearTimeout(this._timer);
delete this._timer;
}
}
};

View File

@ -821,8 +821,6 @@ MarkupView.prototype = {
* Mutation observer used for included nodes.
*/
_mutationObserver: function(aMutations) {
let requiresLayoutChange = false;
for (let mutation of aMutations) {
let type = mutation.type;
let target = mutation.target;
@ -845,11 +843,6 @@ MarkupView.prototype = {
}
if (type === "attributes" || type === "characterData") {
container.update();
// Auto refresh style properties on selected node when they change.
if (type === "attributes" && container.selected) {
requiresLayoutChange = true;
}
} else if (type === "childList" || type === "nativeAnonymousChildList") {
container.childrenDirty = true;
// Update the children to take care of changes in the markup view DOM.
@ -859,10 +852,7 @@ MarkupView.prototype = {
}
}
if (requiresLayoutChange) {
this._inspector.immediateLayoutChange();
}
this._waitForChildren().then((nodes) => {
this._waitForChildren().then(() => {
if (this._destroyer) {
console.warn("Could not fully update after markup mutations, " +
"the markup-view was destroyed while waiting for children.");

View File

@ -7,9 +7,7 @@ support-files =
[browser_responsive_cmd.js]
[browser_responsivecomputedview.js]
skip-if = e10s # Bug ??????
[browser_responsiveruleview.js]
skip-if = e10s # Bug ??????
[browser_responsiveui.js]
skip-if = e10s && os == 'win'
[browser_responsiveui_touch.js]

View File

@ -1,22 +1,11 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
let instance;
"use strict";
let computedView;
let inspector;
// Check that when the viewport is resized, the computed-view refreshes.
waitForExplicitFinish();
let mgr = ResponsiveUI.ResponsiveUIManager;
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
startTest();
}, true);
content.location = "data:text/html;charset=utf-8,<html><style>" +
const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
"div {" +
" width: 500px;" +
" height: 10px;" +
@ -27,74 +16,51 @@ function test() {
" width: 100px;" +
" }" +
"};" +
"</style><div></div></html>"
"</style><div></div></html>";
function computedWidth() {
add_task(function*() {
yield addTab(TEST_URI);
info("Open the responsive design mode and set its size to 500x500 to start");
let {rdm} = yield openRDM();
rdm.setSize(500, 500);
info("Open the inspector, computed-view and select the test node");
let {inspector, view} = yield openComputedView();
yield selectNode("div", inspector);
info("Try shrinking the viewport and checking the applied styles");
yield testShrink(view, inspector, rdm);
info("Try growing the viewport and checking the applied styles");
yield testGrow(view, inspector, rdm);
gBrowser.removeCurrentTab();
});
function* testShrink(computedView, inspector, rdm) {
is(computedWidth(computedView), "500px", "Should show 500px initially.");
let onRefresh = inspector.once("computed-view-refreshed");
rdm.setSize(100, 100);
yield onRefresh;
is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
}
function* testGrow(computedView, inspector, rdm) {
let onRefresh = inspector.once("computed-view-refreshed");
rdm.setSize(500, 500);
yield onRefresh;
is(computedWidth(computedView), "500px", "Should be 500px after growing.");
}
function computedWidth(computedView) {
for (let prop of computedView.propertyViews) {
if (prop.name === "width") {
return prop.valueNode.textContent;
}
}
return null;
}
function startTest() {
document.getElementById("Tools:ResponsiveUI").doCommand();
executeSoon(onUIOpen);
}
function onUIOpen() {
instance = mgr.getResponsiveUIForTab(gBrowser.selectedTab);
ok(instance, "instance of the module is attached to the tab.");
instance.stack.setAttribute("notransition", "true");
registerCleanupFunction(function() {
instance.stack.removeAttribute("notransition");
});
instance.setSize(500, 500);
openComputedView().then(onInspectorUIOpen);
}
function onInspectorUIOpen(args) {
inspector = args.inspector;
computedView = args.view;
ok(inspector, "Got inspector instance");
let div = content.document.getElementsByTagName("div")[0];
inspector.selection.setNode(div);
inspector.once("inspector-updated", testShrink);
}
function testShrink() {
is(computedWidth(), "500px", "Should show 500px initially.");
inspector.once("computed-view-refreshed", function onShrink() {
is(computedWidth(), "100px", "div should be 100px after shrinking.");
testGrow();
});
instance.setSize(100, 100);
}
function testGrow() {
inspector.once("computed-view-refreshed", function onGrow() {
is(computedWidth(), "500px", "Should be 500px after growing.");
finishUp();
});
instance.setSize(500, 500);
}
function finishUp() {
document.getElementById("Tools:ResponsiveUI").doCommand();
// Menus are correctly updated?
is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "false", "menu unchecked");
gBrowser.removeCurrentTab();
finish();
}
}

View File

@ -1,19 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
let instance;
"use strict";
let ruleView;
let inspector;
let mgr = ResponsiveUI.ResponsiveUIManager;
// Check that when the viewport is resized, the rule-view refreshes.
// Also test that ESC does open the split-console, and that the RDM menu item
// gets updated correctly when needed.
// TODO: split this test.
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", startTest, true);
content.location = "data:text/html;charset=utf-8,<html><style>" +
const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
"div {" +
" width: 500px;" +
" height: 10px;" +
@ -24,84 +19,81 @@ function test() {
" width: 100px;" +
" }" +
"};" +
"</style><div></div></html>"
"</style><div></div></html>";
function numberOfRules() {
return ruleView.element.querySelectorAll(".ruleview-code").length;
}
add_task(function*() {
yield addTab(TEST_URI);
function startTest() {
gBrowser.selectedBrowser.removeEventListener("load", startTest, true);
document.getElementById("Tools:ResponsiveUI").doCommand();
executeSoon(onUIOpen);
}
info("Open the responsive design mode and set its size to 500x500 to start");
let {rdm, manager} = yield openRDM();
rdm.setSize(500, 500);
function onUIOpen() {
instance = mgr.getResponsiveUIForTab(gBrowser.selectedTab);
ok(instance, "instance of the module is attached to the tab.");
info("Open the inspector, rule-view and select the test node");
let {inspector, view} = yield openRuleView();
yield selectNode("div", inspector);
instance.stack.setAttribute("notransition", "true");
registerCleanupFunction(function() {
instance.stack.removeAttribute("notransition");
});
info("Try shrinking the viewport and checking the applied styles");
yield testShrink(view, rdm);
instance.setSize(500, 500);
info("Try growing the viewport and checking the applied styles");
yield testGrow(view, rdm);
openRuleView().then(onInspectorUIOpen);
}
info("Check that ESC still opens the split console");
yield testEscapeOpensSplitConsole(inspector);
function onInspectorUIOpen(args) {
inspector = args.inspector;
ruleView = args.view;
ok(inspector, "Got inspector instance");
let div = content.document.getElementsByTagName("div")[0];
inspector.selection.setNode(div);
inspector.once("inspector-updated", testShrink);
}
function testShrink() {
is(numberOfRules(), 2, "Should have two rules initially.");
ruleView.on("ruleview-refreshed", function refresh() {
ruleView.off("ruleview-refreshed", refresh, false);
is(numberOfRules(), 3, "Should have three rules after shrinking.");
testGrow();
}, false);
instance.setSize(100, 100);
}
function testGrow() {
ruleView.on("ruleview-refreshed", function refresh() {
ruleView.off("ruleview-refreshed", refresh, false);
is(numberOfRules(), 2, "Should have two rules after growing.");
testEscapeOpensSplitConsole();
}, false);
instance.setSize(500, 500);
}
function testEscapeOpensSplitConsole() {
is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "true", "menu checked");
ok(!inspector._toolbox._splitConsole, "Console is not split.");
inspector._toolbox.once("split-console", function() {
mgr.once("off", function() {executeSoon(finishUp)});
mgr.toggle(window, gBrowser.selectedTab);
});
EventUtils.synthesizeKey("VK_ESCAPE", {});
}
function finishUp() {
ok(inspector._toolbox._splitConsole, "Console is split after pressing escape.");
// Menus are correctly updated?
is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "false", "menu unchecked");
info("Test the state of the RDM menu item");
yield testMenuItem(manager);
Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
gBrowser.removeCurrentTab();
finish();
}
});
function* testShrink(ruleView, rdm) {
is(numberOfRules(ruleView), 2, "Should have two rules initially.");
info("Resize to 100x100 and wait for the rule-view to update");
let onRefresh = ruleView.once("ruleview-refreshed");
rdm.setSize(100, 100);
yield onRefresh;
is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
}
function* testGrow(ruleView, rdm) {
info("Resize to 500x500 and wait for the rule-view to update");
let onRefresh = ruleView.once("ruleview-refreshed");
rdm.setSize(500, 500);
yield onRefresh;
is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
}
function* testEscapeOpensSplitConsole(inspector) {
ok(!inspector._toolbox._splitConsole, "Console is not split.");
info("Press escape");
let onSplit = inspector._toolbox.once("split-console");
EventUtils.synthesizeKey("VK_ESCAPE", {});
yield onSplit;
ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
}
function* testMenuItem(manager) {
is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
"true",
"The menu item is checked");
info("Toggle off the RDM");
let onManagerOff = manager.once("off");
manager.toggle(window, gBrowser.selectedTab);
yield onManagerOff;
is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"),
"false",
"The menu item is unchecked");
}
function numberOfRules(ruleView) {
return ruleView.element.querySelectorAll(".ruleview-code").length;
}

View File

@ -18,6 +18,26 @@ registerCleanupFunction(() => {
}
});
/**
* Open the Responsive Design Mode
* @param {Tab} The browser tab to open it into (defaults to the selected tab).
* @return {Promise} Resolves to the instance of the responsive design mode.
*/
function openRDM(tab = gBrowser.selectedTab) {
return new Promise(resolve => {
let manager = ResponsiveUI.ResponsiveUIManager;
document.getElementById("Tools:ResponsiveUI").doCommand();
executeSoon(() => {
let rdm = manager.getResponsiveUIForTab(tab);
rdm.stack.setAttribute("notransition", "true");
registerCleanupFunction(function() {
rdm.stack.removeAttribute("notransition");
});
resolve({rdm, manager});
});
});
}
/**
* Open the toolbox, with the inspector tool visible.
* @return a promise that resolves when the inspector is ready
@ -125,7 +145,6 @@ function openRuleView() {
return openInspectorSideBar("ruleview");
}
/**
* Add a new test tab in the browser and load the given url.
* @param {String} url The url to be loaded in the new tab
@ -216,3 +235,36 @@ function waitForDocLoadComplete(aBrowser=gBrowser) {
info("Waiting for browser load");
return deferred.promise;
}
/**
* Get the NodeFront for a node that matches a given css selector, via the
* protocol.
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves to the NodeFront instance
*/
function getNodeFront(selector, {walker}) {
if (selector._form) {
return selector;
}
return walker.querySelector(walker.rootNode, selector);
}
/**
* Set the inspector's current selection to the first match of the given css
* selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
*/
var selectNode = Task.async(function*(selector, inspector, reason = "test") {
info("Selecting the node for '" + selector + "'");
let nodeFront = yield getNodeFront(selector, inspector);
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(nodeFront, reason);
yield updated;
});

View File

@ -6,7 +6,7 @@
"use strict";
const {Cc, Cu, Ci} = require("chrome");
const {Cu} = require("chrome");
const promise = require("promise");
const {Tools} = require("devtools/client/main");
Cu.import("resource://gre/modules/Services.jsm");
@ -37,6 +37,8 @@ function RuleViewTool(inspector, window) {
this.onPropertyChanged = this.onPropertyChanged.bind(this);
this.onViewRefreshed = this.onViewRefreshed.bind(this);
this.onPanelSelected = this.onPanelSelected.bind(this);
this.onMutations = this.onMutations.bind(this);
this.onResized = this.onResized.bind(this);
this.view.on("ruleview-changed", this.onPropertyChanged);
this.view.on("ruleview-refreshed", this.onViewRefreshed);
@ -44,11 +46,12 @@ function RuleViewTool(inspector, window) {
this.inspector.selection.on("detached", this.onSelected);
this.inspector.selection.on("new-node-front", this.onSelected);
this.inspector.on("layout-change", this.refresh);
this.inspector.selection.on("pseudoclass", this.refresh);
this.inspector.target.on("navigate", this.clearUserProperties);
this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
this.inspector.walker.on("mutations", this.onMutations);
this.inspector.walker.on("resize", this.onResized);
this.onSelected();
}
@ -147,8 +150,32 @@ RuleViewTool.prototype = {
this.inspector.emit("rule-view-refreshed");
},
/**
* When markup mutations occur, if an attribute of the selected node changes,
* we need to refresh the view as that might change the node's styles.
*/
onMutations: function(mutations) {
for (let {type, target} of mutations) {
if (target === this.inspector.selection.nodeFront &&
type === "attributes") {
this.refresh();
break;
}
}
},
/**
* When the window gets resized, this may cause media-queries to match, and
* therefore, different styles may apply.
*/
onResized: function() {
this.refresh();
},
destroy: function() {
this.inspector.off("layout-change", this.refresh);
this.inspector.walker.off("mutations", this.onMutations);
this.inspector.walker.off("resize", this.onResized);
this.inspector.selection.off("detached", this.onSelected);
this.inspector.selection.off("pseudoclass", this.refresh);
this.inspector.selection.off("new-node-front", this.onSelected);
this.inspector.target.off("navigate", this.clearUserProperties);
@ -177,13 +204,16 @@ function ComputedViewTool(inspector, window) {
this.onSelected = this.onSelected.bind(this);
this.refresh = this.refresh.bind(this);
this.onPanelSelected = this.onPanelSelected.bind(this);
this.onMutations = this.onMutations.bind(this);
this.onResized = this.onResized.bind(this);
this.inspector.selection.on("detached", this.onSelected);
this.inspector.selection.on("new-node-front", this.onSelected);
this.inspector.on("layout-change", this.refresh);
this.inspector.selection.on("pseudoclass", this.refresh);
this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
this.inspector.walker.on("mutations", this.onMutations);
this.inspector.walker.on("resize", this.onResized);
this.view.selectElement(null);
@ -243,11 +273,35 @@ ComputedViewTool.prototype = {
}
},
/**
* When markup mutations occur, if an attribute of the selected node changes,
* we need to refresh the view as that might change the node's styles.
*/
onMutations: function(mutations) {
for (let {type, target} of mutations) {
if (target === this.inspector.selection.nodeFront &&
type === "attributes") {
this.refresh();
break;
}
}
},
/**
* When the window gets resized, this may cause media-queries to match, and
* therefore, different styles may apply.
*/
onResized: function() {
this.refresh();
},
destroy: function() {
this.inspector.off("layout-change", this.refresh);
this.inspector.walker.off("mutations", this.onMutations);
this.inspector.walker.off("resize", this.onResized);
this.inspector.sidebar.off("computedview-selected", this.refresh);
this.inspector.selection.off("pseudoclass", this.refresh);
this.inspector.selection.off("new-node-front", this.onSelected);
this.inspector.selection.off("detached", this.onSelected);
this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
if (this.inspector.pageStyle) {
this.inspector.pageStyle.off("stylesheet-updated", this.refresh);

View File

@ -1268,29 +1268,36 @@ var WalkerActor = protocol.ActorClass({
typeName: "domwalker",
events: {
"new-mutations" : {
"new-mutations": {
type: "newMutations"
},
"picker-node-picked" : {
"picker-node-picked": {
type: "pickerNodePicked",
node: Arg(0, "disconnectedNode")
},
"picker-node-hovered" : {
"picker-node-hovered": {
type: "pickerNodeHovered",
node: Arg(0, "disconnectedNode")
},
"picker-node-canceled" : {
"picker-node-canceled": {
type: "pickerNodeCanceled"
},
"highlighter-ready" : {
"highlighter-ready": {
type: "highlighter-ready"
},
"highlighter-hide" : {
"highlighter-hide": {
type: "highlighter-hide"
},
"display-change" : {
"display-change": {
type: "display-change",
nodes: Arg(0, "array:domnode")
},
// The walker actor emits a useful "resize" event to its front to let
// clients know when the browser window gets resized. This may be useful
// for refreshing a DOM node's styles for example, since those may depend on
// media-queries.
"resize": {
type: "resize"
}
},
@ -1332,9 +1339,11 @@ var WalkerActor = protocol.ActorClass({
// managed.
this.rootNode = this.document();
this.reflowObserver = getLayoutChangesObserver(this.tabActor);
this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
this._onReflows = this._onReflows.bind(this);
this.reflowObserver.on("reflows", this._onReflows);
this.layoutChangeObserver.on("reflows", this._onReflows);
this._onResize = this._onResize.bind(this);
this.layoutChangeObserver.on("resize", this._onResize);
},
// Returns the JSON representation of this object over the wire.
@ -1395,9 +1404,10 @@ var WalkerActor = protocol.ActorClass({
this.onFrameUnload = null;
this.walkerSearch.destroy();
this.reflowObserver.off("reflows", this._onReflows);
this.reflowObserver = null;
this._onReflows = null;
this.layoutChangeObserver.off("reflows", this._onReflows);
this.layoutChangeObserver.off("resize", this._onResize);
this.layoutChangeObserver = null;
releaseLayoutChangesObserver(this.tabActor);
this.onMutations = null;
@ -1467,6 +1477,13 @@ var WalkerActor = protocol.ActorClass({
}
},
/**
* When the browser window gets resized, relay the event to the front.
*/
_onResize: function() {
events.emit(this, "resize");
},
/**
* This is kept for backward-compatibility reasons with older remote targets.
* Targets prior to bug 916443.
@ -3687,6 +3704,7 @@ var AttributeModificationList = Class({
*/
var InspectorActor = exports.InspectorActor = protocol.ActorClass({
typeName: "inspector",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
@ -3694,6 +3712,7 @@ var InspectorActor = exports.InspectorActor = protocol.ActorClass({
destroy: function () {
protocol.Actor.prototype.destroy.call(this);
this._highlighterPromise = null;
this._pageStylePromise = null;
this._walkerPromise = null;

View File

@ -11,14 +11,14 @@
* 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.
*
* - Observable: A utility parent class, meant at being extended by classes that
* need a to observe something on the tabActor's windows.
*
* - 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.
@ -27,7 +27,7 @@
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 {method, Arg} = protocol;
const events = require("sdk/event/core");
const Heritage = require("sdk/core/heritage");
const {setTimeout, clearTimeout} = require("sdk/timers");
@ -48,7 +48,7 @@ var ReflowActor = exports.ReflowActor = protocol.ActorClass({
* - end {Number}
* - isInterruptible {Boolean}
*/
"reflows" : {
"reflows": {
type: "reflows",
reflows: Arg(0, "array:json")
}
@ -64,9 +64,9 @@ var ReflowActor = exports.ReflowActor = protocol.ActorClass({
},
/**
* 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.
* 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();
@ -132,63 +132,98 @@ exports.ReflowFront = protocol.FrontClass(ReflowActor, {
});
/**
* Base class for all sorts of observers we need to create for a given window.
* Base class for all sorts of observers that need to listen to events on the
* tabActor's windows.
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime the observer observes something
*/
function Observable(tabActor, callback) {
this.tabActor = tabActor;
this.callback = callback;
this._onWindowReady = this._onWindowReady.bind(this);
this._onWindowDestroyed = this._onWindowDestroyed.bind(this);
events.on(this.tabActor, "window-ready", this._onWindowReady);
events.on(this.tabActor, "window-destroyed", this._onWindowDestroyed);
}
Observable.prototype = {
/**
* Is the observer currently observing
*/
observing: false,
isObserving: false,
/**
* Stop observing and detroy this observer instance
*/
destroy: function() {
if (this.isDestroyed) {
return;
}
this.isDestroyed = true;
this.stop();
events.off(this.tabActor, "window-ready", this._onWindowReady);
events.off(this.tabActor, "window-destroyed", this._onWindowDestroyed);
this.callback = null;
this.tabActor = null;
},
/**
* Start observing whatever it is this observer is supposed to observe
*/
start: function() {
if (!this.observing) {
this._start();
this.observing = true;
if (this.isObserving) {
return;
}
},
this.isObserving = true;
_start: function() {
/* To be implemented by sub-classes */
this._startListeners(this.tabActor.windows);
},
/**
* Stop observing
*/
stop: function() {
if (this.observing) {
this._stop();
this.observing = false;
if (!this.isObserving) {
return;
}
this.isObserving = false;
if (this.tabActor.attached && this.tabActor.docShell) {
// It's only worth stopping if the tabActor is still attached
this._stopListeners(this.tabActor.windows);
}
},
_stop: function() {
/* To be implemented by sub-classes */
_onWindowReady: function({window}) {
if (this.isObserving) {
this._startListeners([window]);
}
},
_onWindowDestroyed: function({window}) {
if (this.isObserving) {
this._stopListeners([window]);
}
},
_startListeners: function(windows) {
// To be implemented by sub-classes.
},
_stopListeners: function(windows) {
// 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.tabActor = null;
this.isObserving && this.callback && this.callback.apply(null, args);
}
};
@ -212,7 +247,7 @@ exports.setIgnoreLayoutChanges = function(ignore, syncReflowNode) {
let forceSyncReflow = syncReflowNode.offsetWidth;
}
gIgnoreLayoutChanges = ignore;
}
};
/**
* The LayoutChangesObserver class is instantiated only once per given tab
@ -230,28 +265,31 @@ exports.setIgnoreLayoutChanges = function(ignore, syncReflowNode) {
* corresponding events:
*
* - "reflows", with an array of all the reflows that occured,
* - "resizes", with an array of all the resizes that occured,
*
* @param {TabActor} tabActor
*/
function LayoutChangesObserver(tabActor) {
Observable.call(this, tabActor);
this.tabActor = tabActor;
this._startEventLoop = this._startEventLoop.bind(this);
this._onReflow = this._onReflow.bind(this);
this._onResize = this._onResize.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);
this.resizeObserver = new WindowResizeObserver(this.tabActor, this._onResize);
EventEmitter.decorate(this);
}
exports.LayoutChangesObserver = LayoutChangesObserver;
LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
LayoutChangesObserver.prototype = {
/**
* How long does this observer waits before emitting a batched reflows event.
* How long does this observer waits before emitting batched events.
* 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
@ -264,22 +302,45 @@ LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
* events from being sent.
*/
destroy: function() {
this.isObserving = false;
this.reflowObserver.destroy();
this.reflows = null;
Observable.prototype.destroy.call(this);
this.resizeObserver.destroy();
this.hasResized = false;
this.tabActor = null;
},
_start: function() {
start: function() {
if (this.isObserving) {
return;
}
this.isObserving = true;
this.reflows = [];
this.hasResized = false;
this._startEventLoop();
this.reflowObserver.start();
this.resizeObserver.start();
},
_stop: function() {
stop: function() {
if (!this.isObserving) {
return;
}
this.isObserving = false;
this._stopEventLoop();
this.reflows = [];
this.hasResized = false;
this.reflowObserver.stop();
this.resizeObserver.stop();
},
/**
@ -290,7 +351,7 @@ LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
_startEventLoop: function() {
// Avoid emitting events if the tabActor has been detached (may happen
// during shutdown)
if (!this.tabActor.attached) {
if (!this.tabActor || !this.tabActor.attached) {
return;
}
@ -299,6 +360,13 @@ LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
this.emit("reflows", this.reflows);
this.reflows = [];
}
// Send any resizes we have
if (this.hasResized) {
this.emit("resize");
this.hasResized = false;
}
this.eventLoopTimer = this._setTimeout(this._startEventLoop,
this.EVENT_BATCHING_DELAY);
},
@ -335,8 +403,21 @@ LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
end: end,
isInterruptible: isInterruptible
});
},
/**
* Executed whenever a resize is observed. Only store a flag saying that a
* resize occured.
* The EVENT_BATCHING_DELAY loop will take care of it later.
*/
_onResize: function() {
if (gIgnoreLayoutChanges) {
return;
}
});
this.hasResized = true;
}
};
/**
* Get a LayoutChangesObserver instance for a given window. This function makes
@ -355,12 +436,13 @@ function getLayoutChangesObserver(tabActor) {
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
// counting references allows to stop the observer when no tabActor owns an
// instance.
refCounting: 1
});
obs.start();
return obs;
};
}
exports.getLayoutChangesObserver = getLayoutChangesObserver;
/**
@ -380,52 +462,23 @@ function releaseLayoutChangesObserver(tabActor) {
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.
* Reports any reflow that occurs in the tabActor's docshells.
* @extends Observable
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime a reflow occurs
*/
function ReflowObserver(tabActor, callback) {
Observable.call(this, tabActor, callback);
this._onWindowReady = this._onWindowReady.bind(this);
events.on(this.tabActor, "window-ready", this._onWindowReady);
this._onWindowDestroyed = this._onWindowDestroyed.bind(this);
events.on(this.tabActor, "window-destroyed", this._onWindowDestroyed);
}
ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
Ci.nsISupportsWeakReference]),
_onWindowReady: function({window}) {
if (this.observing) {
this._startListeners([window]);
}
},
_onWindowDestroyed: function({window}) {
if (this.observing) {
this._stopListeners([window]);
}
},
_start: function() {
this._startListeners(this.tabActor.windows);
},
_stop: function() {
if (this.tabActor.attached && this.tabActor.docShell) {
// It's only worth stopping if the tabActor is still attached
this._stopListeners(this.tabActor.windows);
}
},
_startListeners: function(windows) {
for (let window of windows) {
let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
@ -437,14 +490,15 @@ ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
_stopListeners: function(windows) {
for (let window of windows) {
// Corner cases where a global has already been freed may happen, in which
// case, no need to remove the observer
try {
let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docshell.removeWeakReflowObserver(this);
} catch (e) {}
} catch (e) {
// Corner cases where a global has already been freed may happen, in
// which case, no need to remove the observer.
}
}
},
@ -454,16 +508,43 @@ ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
reflowInterruptible: function(start, end) {
this.notifyCallback(start, end, true);
},
destroy: function() {
if (this._isDestroyed) {
return;
}
this._isDestroyed = true;
events.off(this.tabActor, "window-ready", this._onWindowReady);
events.off(this.tabActor, "window-destroyed", this._onWindowDestroyed);
Observable.prototype.destroy.call(this);
}
});
/**
* Reports window resize events on the tabActor's windows.
* @extends Observable
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime a resize occurs
*/
function WindowResizeObserver(tabActor, callback) {
Observable.call(this, tabActor, callback);
this.onResize = this.onResize.bind(this);
}
WindowResizeObserver.prototype = Heritage.extend(Observable.prototype, {
_startListeners: function() {
this.listenerTarget.addEventListener("resize", this.onResize);
},
_stopListeners: function() {
this.listenerTarget.removeEventListener("resize", this.onResize);
},
onResize: function() {
this.notifyCallback();
},
get listenerTarget() {
// For the rootActor, return its window.
if (this.tabActor.isRootActor) {
return this.tabActor.window;
}
// Otherwise, get the tabActor's chromeEventHandler.
return this.tabActor.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
}
});

View File

@ -76,6 +76,7 @@ skip-if = buildapp == 'mulet'
[test_inspector-release.html]
[test_inspector-reload.html]
[test_inspector-remove.html]
[test_inspector-resize.html]
[test_inspector-resolve-url.html]
[test_inspector-retain.html]
[test_inspector-search.html]

View File

@ -0,0 +1,80 @@
<!DOCTYPE HTML>
<html>
<!--
Test that the inspector actor emits "resize" events when the page is resized.
https://bugzilla.mozilla.org/show_bug.cgi?id=1222409
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1222409</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
<script type="application/javascript;version=1.8">
window.onload = function() {
const Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/Loader.jsm");
const {Promise: promise} =
Cu.import("resource://gre/modules/Promise.jsm", {});
const {InspectorFront} =
devtools.require("devtools/server/actors/inspector");
const {console} =
Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {});
SimpleTest.waitForExplicitFinish();
let win = null;
let inspector = null;
addAsyncTest(function* setup() {
info ("Setting up inspector and walker actors.");
let url = document.getElementById("inspectorContent").href;
yield new Promise(resolve => {
attachURL(url, function(err, client, tab, doc) {
win = doc.defaultView;
inspector = InspectorFront(client, tab);
resolve();
});
});
runNextTest();
});
addAsyncTest(function*() {
let walker = yield inspector.getWalker();
// We can't receive events from the walker if we haven't first executed a
// method on the actor to initialize it.
yield walker.querySelector(walker.rootNode, "img");
let {outerWidth, outerHeight} = win;
let onResize = new Promise(resolve => {
walker.once("resize", () => {
resolve();
});
});
win.resizeTo(800, 600);
yield onResize;
ok(true, "The resize event was emitted");
win.resizeTo(outerWidth, outerHeight);
runNextTest();
});
runNextTest();
};
</script>
</head>
<body>
<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>

View File

@ -56,7 +56,26 @@ MockDocShell.prototype = {
addWeakReflowObserver: function(observer) {
this.observer = observer;
},
removeWeakReflowObserver: function(observer) {}
removeWeakReflowObserver: function() {},
get chromeEventHandler() {
return {
addEventListener: (type, cb) => {
if (type === "resize") {
this.resizeCb = cb;
}
},
removeEventListener: (type, cb) => {
if (type === "resize" && cb === this.resizeCb) {
this.resizeCb = null;
}
}
};
},
mockResize: function() {
if (this.resizeCb) {
this.resizeCb();
}
}
};
function run_test() {
@ -110,6 +129,10 @@ function eventsAreBatched() {
let onReflows = (event, reflows) => reflowsEvents.push(reflows);
observer.on("reflows", onReflows);
let resizeEvents = [];
let onResize = () => resizeEvents.push("resize");
observer.on("resize", onResize);
do_print("Fake one reflow event");
tabActor.window.docShell.observer.reflow();
do_print("Checking that no batched reflow event has been emitted");
@ -120,12 +143,21 @@ function eventsAreBatched() {
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");
do_print("Fake a few of resize events too");
tabActor.window.docShell.mockResize();
tabActor.window.docShell.mockResize();
tabActor.window.docShell.mockResize();
do_print("Checking that still no batched resize event has been emitted");
do_check_eq(resizeEvents.length, 0);
do_print("Faking timeout expiration and checking that events are sent");
observer.eventLoopTimer();
do_check_eq(reflowsEvents.length, 1);
do_check_eq(reflowsEvents[0].length, 2);
do_check_eq(resizeEvents.length, 1);
observer.off("reflows", onReflows);
observer.off("resize", onResize);
releaseLayoutChangesObserver(tabActor);
}
@ -153,13 +185,13 @@ function observerIsAlreadyStarted() {
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_check_true(observer.observing);
do_check_true(observer.isObserving);
observer.stop();
do_check_false(observer.observing);
do_check_false(observer.isObserving);
observer.start();
do_check_true(observer.observing);
do_check_true(observer.isObserving);
releaseLayoutChangesObserver(tabActor);
}
@ -169,10 +201,10 @@ function destroyStopsObserving() {
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_check_true(observer.observing);
do_check_true(observer.isObserving);
observer.destroy();
do_check_false(observer.observing);
do_check_false(observer.isObserving);
releaseLayoutChangesObserver(tabActor);
}
@ -184,18 +216,18 @@ function stoppingAndStartingSeveralTimesWorksCorrectly() {
let tabActor = new MockTabActor();
let observer = getLayoutChangesObserver(tabActor);
do_check_true(observer.observing);
do_check_true(observer.isObserving);
observer.start();
observer.start();
observer.start();
do_check_true(observer.observing);
do_check_true(observer.isObserving);
observer.stop();
do_check_false(observer.observing);
do_check_false(observer.isObserving);
observer.stop();
observer.stop();
do_check_false(observer.observing);
do_check_false(observer.isObserving);
releaseLayoutChangesObserver(tabActor);
}