Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2014-05-12 16:21:27 -04:00
commit 7755496a0c
26 changed files with 1405 additions and 437 deletions

View File

@ -200,7 +200,7 @@ const PanelUI = {
}
},
isReady: function() {
get isReady() {
return !!this._isReady;
},

View File

@ -191,7 +191,7 @@ CustomizeMode.prototype = {
// is really not going to work. We pass "true" to ensureReady to
// indicate that we're handling calling startBatchUpdate and
// endBatchUpdate.
if (!window.PanelUI.isReady()) {
if (!window.PanelUI.isReady) {
yield window.PanelUI.ensureReady(true);
}

View File

@ -4,7 +4,6 @@
html {
background-color: #111;
background-image: url("chrome://browser/skin/devtools/noise.png");
}
body {

View File

@ -5,7 +5,7 @@ support-files =
head.js
[browser_layoutview.js]
skip-if = true
[browser_layoutview_rotate-labels-on-sides.js]
[browser_editablemodel.js]
[browser_editablemodel_allproperties.js]
[browser_editablemodel_border.js]

View File

@ -1,145 +1,147 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that editing the box-model values works as expected and test various
// key bindings
const TEST_URI = "<style>" +
"div { margin: 10px; padding: 3px }" +
"#div1 { margin-top: 5px }" +
"#div2 { border-bottom: 1em solid black; }" +
"#div3 { padding: 2em; }" +
"</style>" +
"<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
function getStyle(node, property) {
return node.style.getPropertyValue(property);
}
let doc;
let inspector;
let test = asyncTest(function*() {
let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
let {toolbox, inspector, view} = yield openLayoutView();
let content = yield loadTab("data:text/html," + encodeURIComponent(html));
doc = content.document;
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
inspector = toolbox.getCurrentPanel();
inspector.sidebar.select("layoutview");
yield inspector.sidebar.once("layoutview-ready");
yield runTests();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield gDevTools.closeToolbox(target);
// yield destroyToolbox(inspector);
});
addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",
function*() {
let node = doc.getElementById("div1");
function*(inspector, view) {
let node = content.document.getElementById("div1");
is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.")
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".margin.top > span");
let span = view.doc.querySelector(".margin.top > span");
is(span.textContent, 5, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "5px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("3", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("3", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(getStyle(node, "margin-top"), "3px", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.")
is(span.textContent, 5, "Should have the right value in the box model.");
});
addTest("Test that arrow keys work correctly and pressing enter commits the changes",
function*() {
let node = doc.getElementById("div1");
function*(inspector, view) {
let node = content.document.getElementById("div1");
is(getStyle(node, "margin-left"), "", "Should be no margin-top on the element.")
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".margin.left > span");
let span = view.doc.querySelector(".margin.left > span");
is(span.textContent, 10, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "10px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_UP", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_UP", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "11px", "Should have the right value in the editor.");
is(getStyle(node, "margin-left"), "11px", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_DOWN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_DOWN", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "10px", "Should have the right value in the editor.");
is(getStyle(node, "margin-left"), "10px", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_UP", { shiftKey: true }, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_UP", { shiftKey: true }, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "20px", "Should have the right value in the editor.");
is(getStyle(node, "margin-left"), "20px", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
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.");
});
addTest("Test that deleting the value removes the property but escape undoes that",
function*() {
let node = doc.getElementById("div1");
function*(inspector, view) {
let node = content.document.getElementById("div1");
is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".margin.left > span");
let span = view.doc.querySelector(".margin.left > span");
is(span.textContent, 20, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "20px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_DELETE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "", "Should have the right value in the editor.");
is(getStyle(node, "margin-left"), "", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
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.");
});
addTest("Test that deleting the value removes the property",
function*() {
let node = doc.getElementById("div1");
node.style.marginRight = "15px";
let view = yield selectNode(node);
function*(inspector, view) {
let node = content.document.getElementById("div1");
let span = view.document.querySelector(".margin.right > span");
node.style.marginRight = "15px";
yield waitForUpdate(inspector);
yield selectNode(node, inspector);
let span = view.doc.querySelector(".margin.right > span");
is(span.textContent, 15, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "15px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_DELETE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "", "Should have the right value in the editor.");
is(getStyle(node, "margin-right"), "", "Should have updated the margin.");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
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.");

View File

@ -1,135 +1,144 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test editing box model values when all values are set
const TEST_URI = "<style>" +
"div { margin: 10px; padding: 3px }" +
"#div1 { margin-top: 5px }" +
"#div2 { border-bottom: 1em solid black; }" +
"#div3 { padding: 2em; }" +
"</style>" +
"<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
function getStyle(node, property) {
return node.style.getPropertyValue(property);
}
let doc;
let inspector;
let test = asyncTest(function*() {
let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
let {toolbox, inspector, view} = yield openLayoutView();
let content = yield loadTab("data:text/html," + encodeURIComponent(html));
doc = content.document;
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
inspector = toolbox.getCurrentPanel();
inspector.sidebar.select("layoutview");
yield inspector.sidebar.once("layoutview-ready");
yield runTests();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield gDevTools.closeToolbox(target);
// yield destroyToolbox(inspector);
});
addTest("When all properties are set on the node editing one should work",
function*() {
let node = doc.getElementById("div1");
node.style.padding = "5px";
let view = yield selectNode(node);
function*(inspector, view) {
let node = content.document.getElementById("div1");
let span = view.document.querySelector(".padding.bottom > span");
node.style.padding = "5px";
yield waitForUpdate(inspector);
yield selectNode(node, inspector);
let span = view.doc.querySelector(".padding.bottom > span");
is(span.textContent, 5, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "5px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("7", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("7", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "7", "Should have the right value in the editor.");
is(getStyle(node, "padding-bottom"), "7px", "Should have updated the padding");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
is(getStyle(node, "padding-bottom"), "7px", "Should be the right padding.")
is(span.textContent, 7, "Should have the right value in the box model.");
});
addTest("When all properties are set on the node editing one should work",
function*() {
let node = doc.getElementById("div1");
node.style.padding = "5px";
let view = yield selectNode(node);
function*(inspector, view) {
let node = content.document.getElementById("div1");
let span = view.document.querySelector(".padding.left > span");
node.style.padding = "5px";
yield waitForUpdate(inspector);
yield selectNode(node, inspector);
let span = view.doc.querySelector(".padding.left > span");
is(span.textContent, 5, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "5px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("8", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("8", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "8", "Should have the right value in the editor.");
is(getStyle(node, "padding-left"), "8px", "Should have updated the padding");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(getStyle(node, "padding-left"), "5px", "Should be the right padding.")
is(span.textContent, 5, "Should have the right value in the box model.");
});
addTest("When all properties are set on the node deleting one should work",
function*() {
let node = doc.getElementById("div1");
node.style.padding = "5px";
let view = yield selectNode(node);
function*(inspector, view) {
let node = content.document.getElementById("div1");
let span = view.document.querySelector(".padding.left > span");
node.style.padding = "5px";
yield selectNode(node, inspector);
let span = view.doc.querySelector(".padding.left > span");
is(span.textContent, 5, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "5px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_DELETE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "", "Should have the right value in the editor.");
is(getStyle(node, "padding-left"), "", "Should have updated the padding");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
is(getStyle(node, "padding-left"), "", "Should be the right padding.")
is(span.textContent, 3, "Should have the right value in the box model.");
});
addTest("When all properties are set on the node deleting one then cancelling should work",
function*() {
let node = doc.getElementById("div1");
node.style.padding = "5px";
let view = yield selectNode(node);
function*(inspector, view) {
let node = content.document.getElementById("div1");
let span = view.document.querySelector(".padding.left > span");
node.style.padding = "5px";
yield waitForUpdate(inspector);
yield selectNode(node, inspector);
let span = view.doc.querySelector(".padding.left > span");
is(span.textContent, 5, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "5px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_DELETE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_DELETE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "", "Should have the right value in the editor.");
is(getStyle(node, "padding-left"), "", "Should have updated the padding");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(getStyle(node, "padding-left"), "5px", "Should be the right padding.")
is(span.textContent, 5, "Should have the right value in the box model.");

View File

@ -1,55 +1,56 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that editing the border value in the box model applies the border style
const TEST_URI = "<style>" +
"div { margin: 10px; padding: 3px }" +
"#div1 { margin-top: 5px }" +
"#div2 { border-bottom: 1em solid black; }" +
"#div3 { padding: 2em; }" +
"</style>" +
"<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
function getStyle(node, property) {
return node.style.getPropertyValue(property);
}
let doc;
let inspector;
let test = asyncTest(function*() {
let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
let {toolbox, inspector, view} = yield openLayoutView();
let content = yield loadTab("data:text/html," + encodeURIComponent(html));
doc = content.document;
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
inspector = toolbox.getCurrentPanel();
inspector.sidebar.select("layoutview");
yield inspector.sidebar.once("layoutview-ready");
yield runTests();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield gDevTools.closeToolbox(target);
// yield destroyToolbox(inspector);
});
addTest("Test that adding a border applies a border style when necessary",
function*() {
let node = doc.getElementById("div1");
function*(inspector, view) {
let node = content.document.getElementById("div1");
is(getStyle(node, "border-top-width"), "", "Should have the right border");
is(getStyle(node, "border-top-style"), "", "Should have the right border");
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".border.top > span");
let span = view.doc.querySelector(".border.top > span");
is(span.textContent, 0, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "0", "Should have the right value in the editor.");
EventUtils.synthesizeKey("1", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("1", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "1", "Should have the right value in the editor.");
is(getStyle(node, "border-top-width"), "1px", "Should have the right border");
is(getStyle(node, "border-top-style"), "solid", "Should have the right border");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(getStyle(node, "border-top-width"), "", "Should be the right padding.")
is(getStyle(node, "border-top-style"), "", "Should have the right border");

View File

@ -1,108 +1,107 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that units are displayed correctly when editing values in the box model
// and that values are retrieved and parsed correctly from the back-end
const TEST_URI = "<style>" +
"div { margin: 10px; padding: 3px }" +
"#div1 { margin-top: 5px }" +
"#div2 { border-bottom: 1em solid black; }" +
"#div3 { padding: 2em; }" +
"</style>" +
"<div id='div1'></div><div id='div2'></div><div id='div3'></div>";
function getStyle(node, property) {
return node.style.getPropertyValue(property);
}
let doc;
let inspector;
let test = asyncTest(function*() {
let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }";
let html = "<style>" + style + "</style><div id='div1'></div><div id='div2'></div><div id='div3'></div>"
yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
let {toolbox, inspector, view} = yield openLayoutView();
let content = yield loadTab("data:text/html," + encodeURIComponent(html));
doc = content.document;
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
inspector = toolbox.getCurrentPanel();
inspector.sidebar.select("layoutview");
yield inspector.sidebar.once("layoutview-ready");
yield runTests();
yield runTests(inspector, view);
// TODO: Closing the toolbox in this test leaks - bug 994314
// yield gDevTools.closeToolbox(target);
// yield destroyToolbox(inspector);
});
addTest("Test that entering units works",
function*() {
let node = doc.getElementById("div1");
function*(inspector, view) {
let node = content.document.getElementById("div1");
is(getStyle(node, "padding-top"), "", "Should have the right padding");
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".padding.top > span");
let span = view.doc.querySelector(".padding.top > span");
is(span.textContent, 3, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "3px", "Should have the right value in the editor.");
EventUtils.synthesizeKey("1", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("e", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("1", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
EventUtils.synthesizeKey("e", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(getStyle(node, "padding-top"), "", "An invalid value is handled cleanly");
EventUtils.synthesizeKey("m", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("m", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "1em", "Should have the right value in the editor.");
is(getStyle(node, "padding-top"), "1em", "Should have updated the padding.");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
is(getStyle(node, "padding-top"), "1em", "Should be the right padding.")
is(span.textContent, 16, "Should have the right value in the box model.");
});
addTest("Test that we pick up the value from a higher style rule",
function*() {
let node = doc.getElementById("div2");
function*(inspector, view) {
let node = content.document.getElementById("div2");
is(getStyle(node, "border-bottom-width"), "", "Should have the right border-bottom-width");
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".border.bottom > span");
let span = view.doc.querySelector(".border.bottom > span");
is(span.textContent, 16, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "1em", "Should have the right value in the editor.");
EventUtils.synthesizeKey("0", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("0", {}, view.doc.defaultView);
yield waitForUpdate(inspector);
is(editor.value, "0", "Should have the right value in the editor.");
is(getStyle(node, "border-bottom-width"), "0px", "Should have updated the border.");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
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.");
});
addTest("Test that shorthand properties are parsed correctly",
function*() {
let node = doc.getElementById("div3");
function*(inspector, view) {
let node = content.document.getElementById("div3");
is(getStyle(node, "padding-right"), "", "Should have the right padding");
let view = yield selectNode(node);
yield selectNode(node, inspector);
let span = view.document.querySelector(".padding.right > span");
let span = view.doc.querySelector(".padding.right > span");
is(span.textContent, 32, "Should have the right value in the box model.");
EventUtils.synthesizeMouseAtCenter(span, {}, view);
let editor = view.document.querySelector(".styleinspector-propertyeditor");
EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
let editor = view.doc.querySelector(".styleinspector-propertyeditor");
ok(editor, "Should have opened the editor.");
is(editor.value, "2em", "Should have the right value in the editor.");
EventUtils.synthesizeKey("VK_RETURN", {}, view);
yield waitForUpdate();
EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
is(getStyle(node, "padding-right"), "", "Should be the right padding.")
is(span.textContent, 32, "Should have the right value in the box model.");

View File

@ -1,5 +1,11 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the layout-view displays the right values and that it updates when
// the node's style is changed
// Expected values:
let res1 = [
@ -36,46 +42,22 @@ let res2 = [
{selector: ".border.right > span", value: 10},
];
let inspector;
let view;
let test = asyncTest(function*() {
let style = "div { position: absolute; top: 42px; left: 42px; height: 100px; width: 100px; border: 10px solid black; padding: 20px; margin: 30px auto;}";
let html = "<style>" + style + "</style><div></div>"
let content = yield loadTab("data:text/html," + encodeURIComponent(html));
let node = content.document.querySelector("div");
ok(node, "node found");
yield addTab("data:text/html," + encodeURIComponent(html));
let {toolbox, inspector, view} = yield openLayoutView();
yield selectNode("div", inspector);
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
inspector = toolbox.getCurrentPanel();
yield runTests(inspector, view);
info("Inspector open");
inspector.sidebar.select("layoutview");
yield inspector.sidebar.once("layoutview-ready");
inspector.selection.setNode(node);
yield inspector.once("inspector-updated");
info("Layout view ready");
view = inspector.sidebar.getWindowForTab("layoutview");
ok(!!view.layoutview, "LayoutView document is alive.");
yield runTests();
executeSoon(function() {
inspector._toolbox.destroy();
});
yield gDevTools.once("toolbox-destroyed");
yield destroyToolbox(inspector);
});
addTest("Test that the initial values of the box model are correct",
function*() {
let viewdoc = view.document;
function*(inspector, view) {
let viewdoc = view.doc;
for (let i = 0; i < res1.length; i++) {
let elt = viewdoc.querySelector(res1[i].selector);
@ -84,32 +66,16 @@ function*() {
});
addTest("Test that changing the document updates the box model",
function*() {
let viewdoc = view.document;
function*(inspector, view) {
let viewdoc = view.doc;
let onUpdated = waitForUpdate(inspector);
inspector.selection.node.style.height = "150px";
inspector.selection.node.style.paddingRight = "50px";
yield waitForUpdate();
yield onUpdated;
for (let i = 0; i < res2.length; i++) {
let elt = viewdoc.querySelector(res2[i].selector);
is(elt.textContent, res2[i].value, res2[i].selector + " has the right value after style update.");
}
});
addTest("Test that long labels on left/right are rotated 90 degrees",
function*() {
let viewdoc = view.document;
const LONG_TEXT_ROTATE_LIMIT = 3;
for (let i = 0; i < res1.length; i++) {
let elt = viewdoc.querySelector(res1[i].selector);
let isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT;
let classList = elt.parentNode.classList
let canBeRotated = classList.contains("left") || classList.contains("right");
let isRotated = classList.contains("rotate");
is(canBeRotated && isLong, isRotated, res1[i].selector + " correctly rotated.");
}
});

View File

@ -0,0 +1,48 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that longer values are rotated on the side
const res1 = [
{selector: ".margin.top > span", value: 30},
{selector: ".margin.left > span", value: "auto"},
{selector: ".margin.bottom > span", value: 30},
{selector: ".margin.right > span", value: "auto"},
{selector: ".padding.top > span", value: 20},
{selector: ".padding.left > span", value: 2000000},
{selector: ".padding.bottom > span", value: 20},
{selector: ".padding.right > span", value: 20},
{selector: ".border.top > span", value: 10},
{selector: ".border.left > span", value: 10},
{selector: ".border.bottom > span", value: 10},
{selector: ".border.right > span", value: 10},
];
const TEST_URI = encodeURIComponent([
"<style>",
"div{border:10px solid black; padding: 20px 20px 20px 2000000px; margin: 30px auto;}",
"</style>",
"<div></div>"
].join(""));
const LONG_TEXT_ROTATE_LIMIT = 3;
let test = asyncTest(function*() {
yield addTab("data:text/html," + TEST_URI);
let {toolbox, inspector, view} = yield openLayoutView();
yield selectNode("div", inspector);
for (let i = 0; i < res1.length; i++) {
let elt = view.doc.querySelector(res1[i].selector);
let isLong = elt.textContent.length > LONG_TEXT_ROTATE_LIMIT;
let classList = elt.parentNode.classList
let canBeRotated = classList.contains("left") || classList.contains("right");
let isRotated = classList.contains("rotate");
is(canBeRotated && isLong, isRotated, res1[i].selector + " correctly rotated.");
}
yield destroyToolbox(inspector);
});

View File

@ -1,87 +1,242 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Task.jsm");
"use strict";
const Cu = Components.utils;
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
let TargetFactory = devtools.TargetFactory;
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true);
Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
gDevTools.testing = true;
SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
Services.prefs.clearUserPref("devtools.toolbox.footer.height");
gDevTools.testing = false;
});
// All tests are async in general
// All test are asynchronous
waitForExplicitFinish();
function loadTab(url) {
let deferred = promise.defer();
const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/layoutview/test/";
gBrowser.selectedTab = gBrowser.addTab();
// Uncomment to log events
// Services.prefs.setBoolPref("devtools.dump.emit", true);
// Services.prefs.setBoolPref("devtools.debugger.log", true);
// Set the testing flag on gDevTools and reset it when the test ends
gDevTools.testing = true;
registerCleanupFunction(() => gDevTools.testing = false);
// Clean-up all prefs that might have been changed during a test run
// (safer here because if the test fails, then the pref is never reverted)
Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.dump.emit");
Services.prefs.clearUserPref("devtools.debugger.log");
Services.prefs.clearUserPref("devtools.toolbox.footer.height");
Services.prefs.setCharPref("devtools.inspector.activeSidebar", "ruleview");
});
// Auto close the toolbox and close the test tabs when the test ends
registerCleanupFunction(() => {
// For now, tests must call `yield destroyToolbox(inspector);` at the end.
// This should normally be handled automatically here but some tests
// are leaking when we do so (browser_editablemodel_border.js)
// try {
// let target = TargetFactory.forTab(gBrowser.selectedTab);
// gDevTools.closeToolbox(target);
// } catch (ex) {
// dump(ex);
// }
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
/**
* Define an async test based on a generator function
*/
function asyncTest(generator) {
return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
}
/**
* 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
* @return a promise that resolves to the tab object when the url is loaded
*/
function addTab(url) {
let def = promise.defer();
let tab = gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
waitForFocus(function() {
deferred.resolve(content);
info("URL " + url + " loading complete into new test tab");
waitForFocus(() => {
def.resolve(tab);
}, content);
}, true);
content.location = url;
return deferred.promise;
return def.promise;
}
function selectNode(aNode) {
info("selecting node");
let onSelect = inspector.once("layoutview-updated");
inspector.selection.setNode(aNode, "test");
return onSelect.then(() => {
let view = inspector.sidebar.getWindowForTab("layoutview");
ok(!!view.layoutview, "LayoutView document is alive.");
/**
* Destroy the toolbox
* @param {InspectorPanel}
* @return a promise that resolves when destroyed
*/
let destroyToolbox = Task.async(function*(inspector) {
let onDestroyed = gDevTools.once("toolbox-destroyed");
inspector._toolbox.destroy();
yield onDestroyed;
});
return view;
});
/**
* Simple DOM node accesor function that takes either a node or a string css
* selector as argument and returns the corresponding node
* @param {String|DOMNode} nodeOrSelector
* @return {DOMNode}
*/
function getNode(nodeOrSelector) {
return typeof nodeOrSelector === "string" ?
content.document.querySelector(nodeOrSelector) :
nodeOrSelector;
}
function waitForUpdate() {
/**
* Set the inspector's current selection to a node or to the first match of the
* given css 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
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return a promise that resolves when the inspector is updated with the new
* node
*/
function selectNode(nodeOrSelector, inspector, reason="test") {
info("Selecting the node " + nodeOrSelector);
let node = getNode(nodeOrSelector);
let updated = inspector.once("inspector-updated");
inspector.selection.setNode(node, reason);
return updated;
}
/**
* Open the toolbox, with the inspector tool visible.
* @return a promise that resolves when the inspector is ready
*/
let openInspector = Task.async(function*() {
info("Opening the inspector");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let inspector, toolbox;
// Checking if the toolbox and the inspector are already loaded
// The inspector-updated event should only be waited for if the inspector
// isn't loaded yet
toolbox = gDevTools.getToolbox(target);
if (toolbox) {
inspector = toolbox.getPanel("inspector");
if (inspector) {
info("Toolbox and inspector already open");
return {
toolbox: toolbox,
inspector: inspector
};
}
}
info("Opening the toolbox");
toolbox = yield gDevTools.showToolbox(target, "inspector");
yield waitForToolboxFrameFocus(toolbox);
inspector = toolbox.getPanel("inspector");
info("Waiting for the inspector to update");
yield inspector.once("inspector-updated");
return {
toolbox: toolbox,
inspector: inspector
};
});
/**
* Wait for the toolbox frame to receive focus after it loads
* @param {Toolbox} toolbox
* @return a promise that resolves when focus has been received
*/
function waitForToolboxFrameFocus(toolbox) {
info("Making sure that the toolbox's frame is focused");
let def = promise.defer();
let win = toolbox.frame.contentWindow;
waitForFocus(def.resolve, win);
return def.promise;
}
/**
* Checks whether the inspector's sidebar corresponding to the given id already
* exists
* @param {InspectorPanel}
* @param {String}
* @return {Boolean}
*/
function hasSideBarTab(inspector, id) {
return !!inspector.sidebar.getWindowForTab(id);
}
/**
* Open the toolbox, with the inspector tool visible, and the layout-view
* sidebar tab selected.
* @return a promise that resolves when the inspector is ready and the layout
* view is visible and ready
*/
let openLayoutView = Task.async(function*() {
let {toolbox, inspector} = yield openInspector();
if (!hasSideBarTab(inspector, "layoutview")) {
info("Waiting for the layoutview sidebar to be ready");
yield inspector.sidebar.once("layoutview-ready");
}
info("Selecting the layoutview sidebar");
inspector.sidebar.select("layoutview");
return {
toolbox: toolbox,
inspector: inspector,
view: inspector.sidebar.getWindowForTab("layoutview")["layoutview"]
};
});
/**
* Wait for the layoutview-updated event
* @return a promise
*/
function waitForUpdate(inspector) {
return inspector.once("layoutview-updated");
}
function asyncTest(testfunc) {
return Task.async(function*() {
let initialTab = gBrowser.selectedTab;
yield testfunc();
// Remove all tabs except for the initial tab. This is basically
// gBrowser.removeAllTabsBut without the animation
let tabs = gBrowser.visibleTabs;
gBrowser.selectedTab = initialTab;
for (let i = tabs.length - 1; i >= 0; i--) {
if (tabs[i] != initialTab)
gBrowser.removeTab(tabs[i]);
}
// Reset the sidebar back to the default
Services.prefs.setCharPref("devtools.inspector.activeSidebar", "ruleview");
finish();
});
}
/**
* The addTest/runTests function couple provides a simple way to define
* subsequent test cases in a test file. Example:
*
* addTest("what this test does", function*() {
* ... actual code for the test ...
* });
* addTest("what this second test does", function*() {
* ... actual code for the second test ...
* });
* runTests().then(...);
*/
var TESTS = [];
function addTest(message, func) {
TESTS.push([message, Task.async(func)])
}
var runTests = Task.async(function*() {
let runTests = Task.async(function*(...args) {
for (let [message, test] of TESTS) {
info(message);
yield test();
info("Running new test case: " + message);
yield test.apply(null, args);
}
});

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 {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
const {ReflowFront} = devtools.require("devtools/server/actors/layout");
const NUMERIC = /^-?[\d\.]+$/;
const LONG_TEXT_ROTATE_LIMIT = 3;
@ -90,13 +91,16 @@ EditingSession.prototype = {
let modifications = this._rules[0].startModifyingProperties();
for (let property of properties) {
if (!this._modifications.has(property.name))
this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name));
if (!this._modifications.has(property.name)) {
this._modifications.set(property.name,
this.getPropertyFromRule(this._rules[0], property.name));
}
if (property.value == "")
if (property.value == "") {
modifications.removeProperty(property.name);
else
} else {
modifications.setProperty(property.name, property.value, "");
}
}
return modifications.apply().then(null, console.error);
@ -110,26 +114,33 @@ EditingSession.prototype = {
let modifications = this._rules[0].startModifyingProperties();
for (let [property, value] of this._modifications) {
if (value != "")
if (value != "") {
modifications.setProperty(property, value, "");
else
} else {
modifications.removeProperty(property);
}
}
return modifications.apply().then(null, console.error);
},
destroy: function() {
this._doc = null;
this._rules = null;
this._modifications.clear();
}
};
function LayoutView(aInspector, aWindow)
{
this.inspector = aInspector;
/**
* The layout-view panel
* @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)
if (this.inspector.target.tab) {
this.browser = aInspector.target.tab.linkedBrowser;
}
this.doc = aWindow.document;
this.doc = win.document;
this.sizeLabel = this.doc.querySelector(".size > span");
this.sizeHeadingLabel = this.doc.getElementById("element-size");
@ -137,13 +148,18 @@ function LayoutView(aInspector, aWindow)
}
LayoutView.prototype = {
init: function LV_init() {
init: function() {
this.update = this.update.bind(this);
this.onNewNode = this.onNewNode.bind(this);
this.onNewSelection = this.onNewSelection.bind(this);
this.inspector.selection.on("new-node-front", this.onNewSelection);
this.onNewNode = this.onNewNode.bind(this);
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.
// 'selector' refers to the element that holds the value in view.xhtml;
// 'property' is what we are measuring;
@ -206,7 +222,9 @@ LayoutView.prototype = {
continue;
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);
});
}
@ -214,10 +232,39 @@ LayoutView.prototype = {
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
*/
initEditor: function LV_initEditor(element, event, dimension) {
initEditor: function(element, event, dimension) {
let { property, realProperty } = dimension;
if (!realProperty)
realProperty = property;
@ -233,17 +280,20 @@ LayoutView.prototype = {
},
change: (value) => {
if (NUMERIC.test(value))
if (NUMERIC.test(value)) {
value += "px";
}
let properties = [
{ name: property, value: value }
]
];
if (property.substring(0, 7) == "border-") {
let bprop = property.substring(0, property.length - 5) + "style";
let style = session.getProperty(bprop);
if (!style || style == "none" || style == "hidden")
if (!style || style == "none" || style == "hidden") {
properties.push({ name: bprop, value: "solid" });
}
}
session.setProperties(properties);
@ -251,8 +301,10 @@ LayoutView.prototype = {
done: (value, commit) => {
editor.elt.parentNode.classList.remove("editing");
if (!commit)
if (!commit) {
session.revert();
session.destroy();
}
}
}, event);
},
@ -260,23 +312,35 @@ LayoutView.prototype = {
/**
* Is the layoutview visible in the sidebar?
*/
isActive: function LV_isActive() {
return this.inspector.sidebar.getCurrentTabID() == "layoutview";
isActive: function() {
return this.inspector &&
this.inspector.sidebar.getCurrentTabID() == "layoutview";
},
/**
* Destroy the nodes. Remove listeners.
*/
destroy: function LV_destroy() {
destroy: function() {
this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
this.inspector.selection.off("new-node-front", this.onNewSelection);
if (this.browser) {
this.browser.removeEventListener("MozAfterPaint", this.update, true);
}
this.inspector.sidebar.off("select", this.onSidebarSelect);
this.sizeHeadingLabel = null;
this.sizeLabel = null;
this.inspector = 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() });
},
onNewNode: function LV_onNewNode() {
/**
* @return a promise that resolves when the view has been updated
*/
onNewNode: function() {
if (this.isActive() &&
this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode()) {
@ -295,41 +362,36 @@ LayoutView.prototype = {
} else {
this.dim();
}
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() {
if (this.browser) {
this.browser.removeEventListener("MozAfterPaint", this.update, true);
}
this.trackingPaint = false;
dim: function() {
this.untrackReflows();
this.doc.body.classList.add("dim");
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() {
if (!this.trackingPaint) {
if (this.browser) {
this.browser.addEventListener("MozAfterPaint", this.update, true);
}
this.trackingPaint = true;
}
undim: function() {
this.trackReflows();
this.doc.body.classList.remove("dim");
this.dimmed = false;
},
/**
* Compute the dimensions of the node and update the values in
* the layoutview/view.xhtml document. Returns a promise that will be resolved
* when complete.
* the layoutview/view.xhtml document.
* @return a promise that will be resolved when complete.
*/
update: function LV_update() {
update: function() {
let lastRequest = Task.spawn((function*() {
if (!this.isActive() ||
!this.inspector.selection.isConnected() ||
@ -415,6 +477,10 @@ LayoutView.prototype = {
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={}) {
let toolbox = this.inspector.toolbox;
let nodeFront = this.inspector.selection.nodeFront;
@ -422,6 +488,9 @@ LayoutView.prototype = {
toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
},
/**
* Hide the box-model highlighter on the currently selected element
*/
hideBoxModel: function() {
let toolbox = this.inspector.toolbox;

View File

@ -191,7 +191,7 @@ ElementStyle.prototype = {
return this.dummyElementPromise.then(() => {
if (this.populated != populated) {
// Don't care anymore.
return promise.reject("unused");
return;
}
// Store the current list of rules (if any) during the population

View File

@ -37,6 +37,10 @@ registerCleanupFunction(() => {
// Uncomment to log events
// Services.prefs.setBoolPref("devtools.dump.emit", true);
// Set the testing flag on gDevTools and reset it when the test ends
gDevTools.testing = true;
registerCleanupFunction(() => gDevTools.testing = false);
// Clean-up all prefs that might have been changed during a test run
// (safer here because if the test fails, then the pref is never reverted)
registerCleanupFunction(() => {

View File

@ -231,7 +231,7 @@ panelview:not([mainview]) .toolbarbutton-text,
#PanelUI-contents {
display: block;
flex: auto;
flex: 1 0 auto;
margin-left: auto;
margin-right: auto;
padding: .5em 0;
@ -787,7 +787,7 @@ menuitem.panel-subview-footer@menuStateActive@,
}
/* Popups with only one item don't have a footer */
menupopup[placespopup=true][singleitempopup=true] > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox,
#BMB_bookmarksPopup menupopup[placespopup=true][singleitempopup=true] > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox,
/* These popups never have a footer */
#BMB_bookmarksToolbarPopup > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox,
#BMB_unsortedBookmarksPopup > hbox > .popup-internal-box > .arrowscrollbox-scrollbox > .scrollbox-innerbox {

View File

@ -9,11 +9,15 @@ import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Proxy;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
@ -105,7 +109,9 @@ import android.view.TextureView;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import android.widget.AbsoluteLayout;
import android.widget.Toast;
public class GeckoAppShell
{
@ -2639,4 +2645,77 @@ public class GeckoAppShell
return "DIRECT";
}
/* Downloads the uri pointed to by a share intent, and alters the intent to point to the locally stored file.
*/
public static void downloadImageForIntent(final Intent intent) {
final String src = intent.getStringExtra(Intent.EXTRA_TEXT);
final File dir = GeckoApp.getTempDirectory();
if (dir == null) {
showImageShareFailureToast();
return;
}
GeckoApp.deleteTempFiles();
String type = intent.getType();
OutputStream os = null;
try {
// Create a temporary file for the image
if (src.startsWith("data:")) {
final int dataStart = src.indexOf(",");
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
// If we weren't given an explicit mimetype, try to dig one out of the data uri.
if (TextUtils.isEmpty(extension) && dataStart > 5) {
type = src.substring(5, dataStart).replace(";base64", "");
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
}
final File imageFile = File.createTempFile("image", "." + extension, dir);
os = new FileOutputStream(imageFile);
byte[] buf = Base64.decode(src.substring(dataStart + 1), Base64.DEFAULT);
os.write(buf);
// Only alter the intent when we're sure everything has worked
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
} else {
InputStream is = null;
try {
final byte[] buf = new byte[2048];
final URL url = new URL(src);
final String filename = URLUtil.guessFileName(src, null, type);
is = url.openStream();
final File imageFile = new File(dir, filename);
os = new FileOutputStream(imageFile);
int length;
while ((length = is.read(buf)) != -1) {
os.write(buf, 0, length);
}
// Only alter the intent when we're sure everything has worked
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
} finally {
safeStreamClose(is);
}
}
} catch(IOException ex) {
// If something went wrong, we'll just leave the intent un-changed
} finally {
safeStreamClose(os);
}
}
// Don't fail silently, tell the user that we weren't able to share the image
private static final void showImageShareFailureToast() {
Toast toast = Toast.makeText(getContext(),
getContext().getResources().getString(R.string.share_image_failed),
Toast.LENGTH_SHORT);
toast.show();
}
}

View File

@ -7,12 +7,13 @@ import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.util.ArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.TopSites;
@ -45,12 +46,14 @@ public class TopSitesCursorWrapper implements Cursor {
TopSites.TYPE
};
private static final ArrayMap<String, Integer> columnIndexes =
new ArrayMap<String, Integer>(columnNames.length) {{
private static final Map<String, Integer> columnIndexes =
new HashMap<String, Integer>(columnNames.length);
static {
for (int i = 0; i < columnNames.length; i++) {
put(columnNames[i], i);
columnIndexes.put(columnNames[i], i);
}
}};
}
// Maps column indexes from the wrapper to the cursor's.
private SparseIntArray topIndexes;

View File

@ -8,6 +8,7 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.util.Log;
@ -31,6 +32,7 @@ public class PromptListItem {
public Drawable mIcon;
PromptListItem(JSONObject aObject) {
Context context = GeckoAppShell.getContext();
label = aObject.isNull("label") ? "" : aObject.optString("label");
isGroup = aObject.optBoolean("isGroup");
inGroup = aObject.optBoolean("inGroup");
@ -44,7 +46,8 @@ public class PromptListItem {
String uri = obj.isNull("uri") ? "" : obj.optString("uri");
String type = obj.isNull("type") ? GeckoActionProvider.DEFAULT_MIME_TYPE :
obj.optString("type", GeckoActionProvider.DEFAULT_MIME_TYPE);
mIntent = GeckoAppShell.getShareIntent(GeckoAppShell.getContext(), uri, type, "");
mIntent = GeckoAppShell.getShareIntent(context, uri, type, "");
isParent = true;
} else {
mIntent = null;
@ -55,7 +58,7 @@ public class PromptListItem {
final String iconStr = aObject.optString("icon");
if (iconStr != null) {
BitmapUtils.getDrawable(GeckoAppShell.getContext(), iconStr, new BitmapUtils.BitmapLoader() {
BitmapUtils.getDrawable(context, iconStr, new BitmapUtils.BitmapLoader() {
@Override
public void onBitmapFound(Drawable d) {
mIcon = d;

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.widget;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.menu.MenuItemActionView;
@ -184,6 +185,12 @@ public class GeckoActionProvider {
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
Intent launchIntent = dataModel.chooseActivity(index);
if (launchIntent != null) {
// Share image syncrhonously downloads the image before sharing it.
String type = launchIntent.getType();
if (Intent.ACTION_SEND.equals(launchIntent.getAction()) && type != null && type.startsWith("image/")) {
GeckoAppShell.downloadImageForIntent(launchIntent);
}
launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(launchIntent);
}

View File

@ -29,6 +29,7 @@
[include:gfx/tests/unit/xpcshell.ini]
[include:services/common/tests/unit/xpcshell.ini]
[include:toolkit/components/telemetry/tests/unit/xpcshell.ini]
[include:toolkit/components/osfile/tests/xpcshell/xpcshell.ini]
[include:netwerk/test/unit/xpcshell.ini]
[include:intl/strres/tests/unit/xpcshell.ini]
[include:intl/unicharutil/tests/unit/xpcshell.ini]

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/memory");
this.registerModule("devtools/server/actors/eventlooplag");
this.registerModule("devtools/server/actors/layout");
if ("nsIProfiler" in Ci) {
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_requestTypes.js]
reason = bug 937197
[test_layout-reflows-observer.js]
[test_protocolSpec.js]

View File

@ -1592,6 +1592,8 @@ this.XPIProvider = {
_telemetryDetails: {},
// Experiments are disabled by default. Track ones that are locally enabled.
_enabledExperiments: null,
// A Map from an add-on install to its ID
_addonFileMap: new Map(),
/*
* Set a value in the telemetry hash for a given ID
@ -1636,55 +1638,8 @@ this.XPIProvider = {
* consumers may still have URIs of (leaked) resources they want to map.
*/
_addURIMapping: function XPI__addURIMapping(aID, aFile) {
try {
// Always use our own mechanics instead of nsIIOService.newFileURI, so
// that we can be sure to map things as we want them mapped.
let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, "."));
if (!uri) {
throw new Error("Cannot resolve");
}
this._ensureURIMappings();
this._uriMappings[aID] = uri.spec;
}
catch (ex) {
logger.warn("Failed to add URI mapping", ex);
}
},
/**
* Ensures that the URI to Addon mappings are available.
*
* The function will add mappings for all non-bootstrapped but enabled
* add-ons.
* Bootstrapped add-on mappings will be added directly when the bootstrap
* scope get loaded. (See XPIProvider._addURIMapping() and callers)
*/
_ensureURIMappings: function XPI__ensureURIMappings() {
if (this._uriMappings) {
return;
}
// XXX Convert to Map(), once it gets stable with stable iterators
this._uriMappings = Object.create(null);
// XXX Convert to Set(), once it gets stable with stable iterators
let enabled = Object.create(null);
let enabledAddons = this.enabledAddons || "";
for (let a of enabledAddons.split(",")) {
a = decodeURIComponent(a.split(":")[0]);
enabled[a] = null;
}
let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"));
for (let loc of cache) {
for (let [id, val] in Iterator(loc.addons)) {
if (!(id in enabled)) {
continue;
}
let file = new nsIFile(val.descriptor);
let spec = Services.io.newFileURI(file).spec;
this._uriMappings[id] = spec;
}
}
logger.info("Mapping " + aID + " to " + aFile.path);
this._addonFileMap.set(aID, aFile.path);
},
/**
@ -1914,10 +1869,6 @@ this.XPIProvider = {
this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, "");
// Invalidate the URI mappings now that |enabledAddons| was updated.
// |_ensureMappings()| will re-create the mappings when needed.
delete this._uriMappings;
if ("nsICrashReporter" in Ci &&
Services.appinfo instanceof Ci.nsICrashReporter) {
// Annotate the crash report with relevant add-on information.
@ -2027,9 +1978,7 @@ this.XPIProvider = {
// This is needed to allow xpcshell tests to simulate a restart
this.extensionsActive = false;
// Remove URI mappings again
delete this._uriMappings;
this._addonFileMap.clear();
if (gLazyObjectsLoaded) {
let done = XPIDatabase.shutdown();
@ -3715,17 +3664,15 @@ this.XPIProvider = {
* @see amIAddonManager.mapURIToAddonID
*/
mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
this._ensureURIMappings();
let resolved = this._resolveURIToFile(aURI);
if (!resolved) {
if (!resolved || !(resolved instanceof Ci.nsIFileURL))
return null;
}
resolved = resolved.spec;
for (let [id, spec] in Iterator(this._uriMappings)) {
if (resolved.startsWith(spec)) {
for (let [id, path] of this._addonFileMap) {
if (resolved.file.path.startsWith(path))
return id;
}
}
return null;
},
@ -4120,9 +4067,6 @@ this.XPIProvider = {
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
createInstance(Ci.mozIJSSubScriptLoader);
// Add a mapping for XPIProvider.mapURIToAddonID
this._addURIMapping(aId, aFile);
try {
// Copy the reason values from the global object into the bootstrap scope.
for (let name in BOOTSTRAP_REASONS)
@ -6945,6 +6889,7 @@ DirectoryInstallLocation.prototype = {
this._IDToFileMap[id] = entry;
this._FileToIDMap[entry.path] = id;
XPIProvider._addURIMapping(id, entry);
}
},
@ -7154,6 +7099,7 @@ DirectoryInstallLocation.prototype = {
}
this._FileToIDMap[newFile.path] = aId;
this._IDToFileMap[aId] = newFile;
XPIProvider._addURIMapping(aId, newFile);
if (aExistingAddonID && aExistingAddonID != aId &&
aExistingAddonID in this._IDToFileMap) {
@ -7348,6 +7294,7 @@ WinRegInstallLocation.prototype = {
this._IDToFileMap[id] = file;
this._FileToIDMap[file.path] = id;
XPIProvider._addURIMapping(id, file);
}
},

View File

@ -179,12 +179,12 @@ function run_test_2(uri) {
});
}
// Tests that add-on URIs aren't mappable if the add-on was never started in a
// Tests that add-on URIs are mappable if the add-on was never started in a
// session
function run_test_3(uri) {
restartManager();
do_check_eq(AddonManager.mapURIToAddonID(uri), null);
check_mapping(uri, "bootstrap1@tests.mozilla.org");
run_test_4();
}
@ -217,6 +217,43 @@ function run_test_4() {
function run_test_5() {
restartManager();
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
let uri = b1.getResourceURI(".");
check_mapping(uri, b1.id);
do_execute_soon(run_test_6);
});
}
// Tests that add-on URIs are mappable after being uninstalled
function run_test_6() {
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
prepare_test({
"bootstrap1@tests.mozilla.org": [
["onUninstalling", false],
"onUninstalled"
]
});
let uri = b1.getResourceURI(".");
b1.uninstall();
ensure_test_completed();
check_mapping(uri, b1.id);
restartManager();
do_execute_soon(run_test_7);
});
}
// Tests that add-on URIs are mappable for add-ons detected at startup
function run_test_7() {
shutdownManager();
manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, "bootstrap1@tests.mozilla.org");
startupManager();
AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
let uri = b1.getResourceURI(".");
check_mapping(uri, b1.id);