summaryrefslogtreecommitdiff
path: root/toolkit/devtools/shared/test
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/shared/test')
-rw-r--r--toolkit/devtools/shared/test/browser.ini98
-rw-r--r--toolkit/devtools/shared/test/browser_css_color.js316
-rw-r--r--toolkit/devtools/shared/test/browser_cubic-bezier-01.js37
-rw-r--r--toolkit/devtools/shared/test/browser_cubic-bezier-02.js149
-rw-r--r--toolkit/devtools/shared/test/browser_cubic-bezier-03.js68
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-01.js57
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-02.js42
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-03a.js120
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-03b.js76
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-04.js90
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-utils-01.js261
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-utils-02.js103
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-utils-03.js112
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-utils-04.js166
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-utils-05.js42
-rw-r--r--toolkit/devtools/shared/test/browser_flame-graph-utils-hash.js24
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-01.js66
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-02.js83
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-03.js110
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-04.js68
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-05.js132
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-06.js90
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-07a.js201
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-07b.js66
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-08.js66
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-09a.js82
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-09b.js60
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-09c.js37
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-09d.js38
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-09e.js62
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-09f.js52
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-10a.js139
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-10b.js48
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-11a.js59
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-11b.js129
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-12.js153
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-13.js42
-rw-r--r--toolkit/devtools/shared/test/browser_graphs-14.js89
-rw-r--r--toolkit/devtools/shared/test/browser_inplace-editor.js123
-rw-r--r--toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.html65
-rw-r--r--toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js222
-rw-r--r--toolkit/devtools/shared/test/browser_layoutHelpers.html25
-rw-r--r--toolkit/devtools/shared/test/browser_layoutHelpers.js101
-rw-r--r--toolkit/devtools/shared/test/browser_layoutHelpers_iframe.html19
-rw-r--r--toolkit/devtools/shared/test/browser_num-l10n.js25
-rw-r--r--toolkit/devtools/shared/test/browser_observableobject.js86
-rw-r--r--toolkit/devtools/shared/test/browser_options-view-01.js101
-rw-r--r--toolkit/devtools/shared/test/browser_outputparser.js102
-rw-r--r--toolkit/devtools/shared/test/browser_prefs.js33
-rw-r--r--toolkit/devtools/shared/test/browser_require_basic.js140
-rw-r--r--toolkit/devtools/shared/test/browser_spectrum.js114
-rw-r--r--toolkit/devtools/shared/test/browser_tableWidget_basic.js382
-rw-r--r--toolkit/devtools/shared/test/browser_tableWidget_keyboard_interaction.js227
-rw-r--r--toolkit/devtools/shared/test/browser_tableWidget_mouse_interaction.js298
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_button_eyedropper.js57
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_button_paintflashing.js89
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_button_responsive.js89
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_button_scratchpad.js127
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_button_tilt.js89
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_sidebar.js85
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolbox.js20
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js27
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js20
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js20
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js19
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js20
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_options.js19
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_shadereditor.js33
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js25
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js20
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js26
-rw-r--r--toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js19
-rw-r--r--toolkit/devtools/shared/test/browser_templater_basic.html13
-rw-r--r--toolkit/devtools/shared/test/browser_templater_basic.js285
-rw-r--r--toolkit/devtools/shared/test/browser_theme.js81
-rw-r--r--toolkit/devtools/shared/test/browser_toolbar_basic.html35
-rw-r--r--toolkit/devtools/shared/test/browser_toolbar_basic.js74
-rw-r--r--toolkit/devtools/shared/test/browser_toolbar_tooltip.js72
-rw-r--r--toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.html32
-rw-r--r--toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.js246
-rw-r--r--toolkit/devtools/shared/test/browser_treeWidget_basic.js254
-rw-r--r--toolkit/devtools/shared/test/browser_treeWidget_keyboard_interaction.js228
-rw-r--r--toolkit/devtools/shared/test/browser_treeWidget_mouse_interaction.js137
-rw-r--r--toolkit/devtools/shared/test/doc_options-view.xul27
-rw-r--r--toolkit/devtools/shared/test/head.js241
-rw-r--r--toolkit/devtools/shared/test/leakhunt.js170
-rw-r--r--toolkit/devtools/shared/test/unit/test_VariablesView_getString_promise.js75
-rw-r--r--toolkit/devtools/shared/test/unit/test_bezierCanvas.js113
-rw-r--r--toolkit/devtools/shared/test/unit/test_cubicBezier.js102
-rw-r--r--toolkit/devtools/shared/test/unit/test_undoStack.js98
-rw-r--r--toolkit/devtools/shared/test/unit/xpcshell.ini10
91 files changed, 8793 insertions, 0 deletions
diff --git a/toolkit/devtools/shared/test/browser.ini b/toolkit/devtools/shared/test/browser.ini
new file mode 100644
index 000000000..096238c21
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser.ini
@@ -0,0 +1,98 @@
+[DEFAULT]
+subsuite = devtools
+support-files =
+ browser_layoutHelpers.html
+ browser_layoutHelpers-getBoxQuads.html
+ browser_layoutHelpers_iframe.html
+ browser_templater_basic.html
+ browser_toolbar_basic.html
+ browser_toolbar_webconsole_errors_count.html
+ doc_options-view.xul
+ head.js
+ leakhunt.js
+
+[browser_css_color.js]
+[browser_cubic-bezier-01.js]
+[browser_cubic-bezier-02.js]
+[browser_cubic-bezier-03.js]
+[browser_flame-graph-01.js]
+[browser_flame-graph-02.js]
+[browser_flame-graph-03a.js]
+[browser_flame-graph-03b.js]
+[browser_flame-graph-04.js]
+[browser_flame-graph-utils-01.js]
+[browser_flame-graph-utils-02.js]
+[browser_flame-graph-utils-03.js]
+[browser_flame-graph-utils-04.js]
+[browser_flame-graph-utils-05.js]
+[browser_flame-graph-utils-hash.js]
+[browser_graphs-01.js]
+[browser_graphs-02.js]
+[browser_graphs-03.js]
+[browser_graphs-04.js]
+[browser_graphs-05.js]
+[browser_graphs-06.js]
+[browser_graphs-07a.js]
+[browser_graphs-07b.js]
+[browser_graphs-08.js]
+[browser_graphs-09a.js]
+[browser_graphs-09b.js]
+[browser_graphs-09c.js]
+[browser_graphs-09d.js]
+[browser_graphs-09e.js]
+[browser_graphs-09f.js]
+[browser_graphs-10a.js]
+[browser_graphs-10b.js]
+[browser_graphs-11a.js]
+[browser_graphs-11b.js]
+[browser_graphs-12.js]
+[browser_graphs-13.js]
+[browser_graphs-14.js]
+[browser_inplace-editor.js]
+[browser_layoutHelpers.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_layoutHelpers-getBoxQuads.js]
+skip-if = e10s # Layouthelpers test should not run in a content page.
+[browser_num-l10n.js]
+[browser_observableobject.js]
+[browser_options-view-01.js]
+[browser_outputparser.js]
+skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
+[browser_prefs.js]
+[browser_require_basic.js]
+[browser_spectrum.js]
+[browser_theme.js]
+[browser_tableWidget_basic.js]
+[browser_tableWidget_keyboard_interaction.js]
+[browser_tableWidget_mouse_interaction.js]
+skip-if = buildapp == 'mulet'
+[browser_telemetry_button_eyedropper.js]
+[browser_telemetry_button_paintflashing.js]
+skip-if = e10s # Bug 937167 - e10s paintflashing
+[browser_telemetry_button_responsive.js]
+skip-if = e10s # Bug 1067145 - e10s responsiveview
+[browser_telemetry_button_scratchpad.js]
+[browser_telemetry_button_tilt.js]
+skip-if = e10s # Bug 1086492 - Disable tilt for e10s
+ # Bug 937166 - Make tilt work in E10S mode
+[browser_telemetry_sidebar.js]
+[browser_telemetry_toolbox.js]
+[browser_telemetry_toolboxtabs_canvasdebugger.js]
+[browser_telemetry_toolboxtabs_inspector.js]
+[browser_telemetry_toolboxtabs_jsdebugger.js]
+[browser_telemetry_toolboxtabs_jsprofiler.js]
+[browser_telemetry_toolboxtabs_netmonitor.js]
+[browser_telemetry_toolboxtabs_options.js]
+[browser_telemetry_toolboxtabs_shadereditor.js]
+[browser_telemetry_toolboxtabs_storage.js]
+[browser_telemetry_toolboxtabs_styleeditor.js]
+[browser_telemetry_toolboxtabs_webaudioeditor.js]
+[browser_telemetry_toolboxtabs_webconsole.js]
+[browser_templater_basic.js]
+[browser_toolbar_basic.js]
+[browser_toolbar_tooltip.js]
+[browser_toolbar_webconsole_errors_count.js]
+skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't correct with e10s
+[browser_treeWidget_basic.js]
+[browser_treeWidget_keyboard_interaction.js]
+[browser_treeWidget_mouse_interaction.js]
diff --git a/toolkit/devtools/shared/test/browser_css_color.js b/toolkit/devtools/shared/test/browser_css_color.js
new file mode 100644
index 000000000..4f4f29be5
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_css_color.js
@@ -0,0 +1,316 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
+let {colorUtils} = devtools.require("devtools/css-color");
+let origColorUnit;
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+ origColorUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
+
+ info("Creating a test canvas element to test colors");
+ let canvas = createTestCanvas(doc);
+ info("Starting the test");
+ testColorUtils(canvas);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function createTestCanvas(doc) {
+ let canvas = doc.createElement("canvas");
+ canvas.width = canvas.height = 10;
+ doc.body.appendChild(canvas);
+ return canvas;
+}
+
+function testColorUtils(canvas) {
+ let data = getTestData();
+
+ for (let {authored, name, hex, hsl, rgb} of data) {
+ let color = new colorUtils.CssColor(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(color.name, name, "color.name === name");
+ is(color.hex, hex, "color.hex === hex");
+ is(color.hsl, hsl, "color.hsl === hsl");
+ is(color.rgb, rgb, "color.rgb === rgb");
+
+ testToString(color, name, hex, hsl, rgb);
+ testColorMatch(name, hex, hsl, rgb, color.rgba, canvas);
+ }
+
+ testProcessCSSString();
+ testSetAlpha();
+}
+
+function testToString(color, name, hex, hsl, rgb) {
+ switchColorUnit(colorUtils.CssColor.COLORUNIT.name);
+ is(color.toString(), name, "toString() with authored type");
+
+ switchColorUnit(colorUtils.CssColor.COLORUNIT.hex);
+ is(color.toString(), hex, "toString() with hex type");
+
+ switchColorUnit(colorUtils.CssColor.COLORUNIT.hsl);
+ is(color.toString(), hsl, "toString() with hsl type");
+
+ switchColorUnit(colorUtils.CssColor.COLORUNIT.rgb);
+ is(color.toString(), rgb, "toString() with rgb type");
+}
+
+function switchColorUnit(unit) {
+ Services.prefs.setCharPref(COLOR_UNIT_PREF, unit);
+}
+
+function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
+ let target;
+ let ctx = canvas.getContext("2d");
+
+ let clearCanvas = function() {
+ canvas.width = 1;
+ };
+ let setColor = function(aColor) {
+ ctx.fillStyle = aColor;
+ ctx.fillRect(0, 0, 1, 1);
+ };
+ let setTargetColor = function() {
+ clearCanvas();
+ // All colors have rgba so we can use this to compare against.
+ setColor(rgba);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+ target = {r: r, g: g, b: b, a: a};
+ };
+ let test = function(aColor, type) {
+ let tolerance = 3; // hsla -> rgba -> hsla produces inaccurate results so we
+ // need some tolerence here.
+ clearCanvas();
+
+ setColor(aColor);
+ let [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
+
+ let rgbFail = Math.abs(r - target.r) > tolerance ||
+ Math.abs(g - target.g) > tolerance ||
+ Math.abs(b - target.b) > tolerance;
+ ok(!rgbFail, "color " + rgba + " matches target. Type: " + type);
+ if (rgbFail) {
+ info("target: " + (target.toSource()) + ", color: [r: " + r + ", g: " + g + ", b: " + b + ", a: " + a + "]");
+ }
+
+ let alphaFail = a !== target.a;
+ ok(!alphaFail, "color " + rgba + " alpha value matches target.");
+ };
+
+ setTargetColor();
+
+ test(name, "name");
+ test(hex, "hex");
+ test(hsl, "hsl");
+ test(rgb, "rgb");
+ switchColorUnit(origColorUnit);
+}
+
+function testProcessCSSString() {
+ let before = "border: 1px solid red; border-radius: 5px; " +
+ "color rgb(0, 255, 0); font-weight: bold; " +
+ "background-color: transparent; " +
+ "border-top-color: rgba(0, 0, 255, 0.5);";
+ let expected = "border: 1px solid #F00; border-radius: 5px; " +
+ "color #0F0; font-weight: bold; " +
+ "background-color: transparent; " +
+ "border-top-color: rgba(0, 0, 255, 0.5);";
+ let after = colorUtils.processCSSString(before);
+
+ is(after, expected, "CSS string processed correctly");
+}
+
+function testSetAlpha() {
+ let values = [
+ ["longhex", "#ff0000", 0.5, "rgba(255, 0, 0, 0.5)"],
+ ["hex", "#f0f", 0.2, "rgba(255, 0, 255, 0.2)"],
+ ["rgba", "rgba(120, 34, 23, 1)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["rgb", "rgb(120, 34, 23)", 0.25, "rgba(120, 34, 23, 0.25)"],
+ ["hsl", "hsl(208, 100%, 97%)", 0.75, "rgba(239, 247, 255, 0.75)"],
+ ["hsla", "hsla(208, 100%, 97%, 1)", 0.75, "rgba(239, 247, 255, 0.75)"]
+ ];
+ values.forEach(([type, value, alpha, expected]) => {
+ is(colorUtils.setAlpha(value, alpha), expected, "correctly sets alpha value for " + type);
+ });
+
+ try {
+ colorUtils.setAlpha("rgb(24, 25, 45, 1)", 1);
+ ok(false, "Should fail when passing in an invalid color.");
+ } catch (e) {
+ ok(true, "Fails when setAlpha receives an invalid color.");
+ }
+
+ is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)", "sets alpha to 1 if invalid.");
+}
+
+function getTestData() {
+ return [
+ {authored: "aliceblue", name: "aliceblue", hex: "#F0F8FF", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
+ {authored: "antiquewhite", name: "antiquewhite", hex: "#FAEBD7", hsl: "hsl(34, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
+ {authored: "aqua", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
+ {authored: "aquamarine", name: "aquamarine", hex: "#7FFFD4", hsl: "hsl(160, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
+ {authored: "azure", name: "azure", hex: "#F0FFFF", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
+ {authored: "beige", name: "beige", hex: "#F5F5DC", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
+ {authored: "bisque", name: "bisque", hex: "#FFE4C4", hsl: "hsl(33, 100%, 88%)", rgb: "rgb(255, 228, 196)"},
+ {authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)"},
+ {authored: "blanchedalmond", name: "blanchedalmond", hex: "#FFEBCD", hsl: "hsl(36, 100%, 90%)", rgb: "rgb(255, 235, 205)"},
+ {authored: "blue", name: "blue", hex: "#00F", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)"},
+ {authored: "blueviolet", name: "blueviolet", hex: "#8A2BE2", hsl: "hsl(271, 76%, 53%)", rgb: "rgb(138, 43, 226)"},
+ {authored: "brown", name: "brown", hex: "#A52A2A", hsl: "hsl(0, 59%, 41%)", rgb: "rgb(165, 42, 42)"},
+ {authored: "burlywood", name: "burlywood", hex: "#DEB887", hsl: "hsl(34, 57%, 70%)", rgb: "rgb(222, 184, 135)"},
+ {authored: "cadetblue", name: "cadetblue", hex: "#5F9EA0", hsl: "hsl(182, 25%, 50%)", rgb: "rgb(95, 158, 160)"},
+ {authored: "chartreuse", name: "chartreuse", hex: "#7FFF00", hsl: "hsl(90, 100%, 50%)", rgb: "rgb(127, 255, 0)"},
+ {authored: "chocolate", name: "chocolate", hex: "#D2691E", hsl: "hsl(25, 75%, 47%)", rgb: "rgb(210, 105, 30)"},
+ {authored: "coral", name: "coral", hex: "#FF7F50", hsl: "hsl(16, 100%, 66%)", rgb: "rgb(255, 127, 80)"},
+ {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ED", hsl: "hsl(219, 79%, 66%)", rgb: "rgb(100, 149, 237)"},
+ {authored: "cornsilk", name: "cornsilk", hex: "#FFF8DC", hsl: "hsl(48, 100%, 93%)", rgb: "rgb(255, 248, 220)"},
+ {authored: "crimson", name: "crimson", hex: "#DC143C", hsl: "hsl(348, 83%, 47%)", rgb: "rgb(220, 20, 60)"},
+ {authored: "cyan", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
+ {authored: "darkblue", name: "darkblue", hex: "#00008B", hsl: "hsl(240, 100%, 27%)", rgb: "rgb(0, 0, 139)"},
+ {authored: "darkcyan", name: "darkcyan", hex: "#008B8B", hsl: "hsl(180, 100%, 27%)", rgb: "rgb(0, 139, 139)"},
+ {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#B8860B", hsl: "hsl(43, 89%, 38%)", rgb: "rgb(184, 134, 11)"},
+ {authored: "darkgray", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
+ {authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 20%)", rgb: "rgb(0, 100, 0)"},
+ {authored: "darkgrey", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
+ {authored: "darkkhaki", name: "darkkhaki", hex: "#BDB76B", hsl: "hsl(56, 38%, 58%)", rgb: "rgb(189, 183, 107)"},
+ {authored: "darkmagenta", name: "darkmagenta", hex: "#8B008B", hsl: "hsl(300, 100%, 27%)", rgb: "rgb(139, 0, 139)"},
+ {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556B2F", hsl: "hsl(82, 39%, 30%)", rgb: "rgb(85, 107, 47)"},
+ {authored: "darkorange", name: "darkorange", hex: "#FF8C00", hsl: "hsl(33, 100%, 50%)", rgb: "rgb(255, 140, 0)"},
+ {authored: "darkorchid", name: "darkorchid", hex: "#9932CC", hsl: "hsl(280, 61%, 50%)", rgb: "rgb(153, 50, 204)"},
+ {authored: "darkred", name: "darkred", hex: "#8B0000", hsl: "hsl(0, 100%, 27%)", rgb: "rgb(139, 0, 0)"},
+ {authored: "darksalmon", name: "darksalmon", hex: "#E9967A", hsl: "hsl(15, 72%, 70%)", rgb: "rgb(233, 150, 122)"},
+ {authored: "darkseagreen", name: "darkseagreen", hex: "#8FBC8F", hsl: "hsl(120, 25%, 65%)", rgb: "rgb(143, 188, 143)"},
+ {authored: "darkslateblue", name: "darkslateblue", hex: "#483D8B", hsl: "hsl(248, 39%, 39%)", rgb: "rgb(72, 61, 139)"},
+ {authored: "darkslategray", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
+ {authored: "darkslategrey", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
+ {authored: "darkturquoise", name: "darkturquoise", hex: "#00CED1", hsl: "hsl(181, 100%, 41%)", rgb: "rgb(0, 206, 209)"},
+ {authored: "darkviolet", name: "darkviolet", hex: "#9400D3", hsl: "hsl(282, 100%, 41%)", rgb: "rgb(148, 0, 211)"},
+ {authored: "deeppink", name: "deeppink", hex: "#FF1493", hsl: "hsl(328, 100%, 54%)", rgb: "rgb(255, 20, 147)"},
+ {authored: "deepskyblue", name: "deepskyblue", hex: "#00BFFF", hsl: "hsl(195, 100%, 50%)", rgb: "rgb(0, 191, 255)"},
+ {authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41%)", rgb: "rgb(105, 105, 105)"},
+ {authored: "dodgerblue", name: "dodgerblue", hex: "#1E90FF", hsl: "hsl(210, 100%, 56%)", rgb: "rgb(30, 144, 255)"},
+ {authored: "firebrick", name: "firebrick", hex: "#B22222", hsl: "hsl(0, 68%, 42%)", rgb: "rgb(178, 34, 34)"},
+ {authored: "floralwhite", name: "floralwhite", hex: "#FFFAF0", hsl: "hsl(40, 100%, 97%)", rgb: "rgb(255, 250, 240)"},
+ {authored: "forestgreen", name: "forestgreen", hex: "#228B22", hsl: "hsl(120, 61%, 34%)", rgb: "rgb(34, 139, 34)"},
+ {authored: "fuchsia", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
+ {authored: "gainsboro", name: "gainsboro", hex: "#DCDCDC", hsl: "hsl(0, 0%, 86%)", rgb: "rgb(220, 220, 220)"},
+ {authored: "ghostwhite", name: "ghostwhite", hex: "#F8F8FF", hsl: "hsl(240, 100%, 99%)", rgb: "rgb(248, 248, 255)"},
+ {authored: "gold", name: "gold", hex: "#FFD700", hsl: "hsl(51, 100%, 50%)", rgb: "rgb(255, 215, 0)"},
+ {authored: "goldenrod", name: "goldenrod", hex: "#DAA520", hsl: "hsl(43, 74%, 49%)", rgb: "rgb(218, 165, 32)"},
+ {authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
+ {authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25%)", rgb: "rgb(0, 128, 0)"},
+ {authored: "greenyellow", name: "greenyellow", hex: "#ADFF2F", hsl: "hsl(84, 100%, 59%)", rgb: "rgb(173, 255, 47)"},
+ {authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
+ {authored: "honeydew", name: "honeydew", hex: "#F0FFF0", hsl: "hsl(120, 100%, 97%)", rgb: "rgb(240, 255, 240)"},
+ {authored: "hotpink", name: "hotpink", hex: "#FF69B4", hsl: "hsl(330, 100%, 71%)", rgb: "rgb(255, 105, 180)"},
+ {authored: "indianred", name: "indianred", hex: "#CD5C5C", hsl: "hsl(0, 53%, 58%)", rgb: "rgb(205, 92, 92)"},
+ {authored: "indigo", name: "indigo", hex: "#4B0082", hsl: "hsl(275, 100%, 25%)", rgb: "rgb(75, 0, 130)"},
+ {authored: "ivory", name: "ivory", hex: "#FFFFF0", hsl: "hsl(60, 100%, 97%)", rgb: "rgb(255, 255, 240)"},
+ {authored: "khaki", name: "khaki", hex: "#F0E68C", hsl: "hsl(54, 77%, 75%)", rgb: "rgb(240, 230, 140)"},
+ {authored: "lavender", name: "lavender", hex: "#E6E6FA", hsl: "hsl(240, 67%, 94%)", rgb: "rgb(230, 230, 250)"},
+ {authored: "lavenderblush", name: "lavenderblush", hex: "#FFF0F5", hsl: "hsl(340, 100%, 97%)", rgb: "rgb(255, 240, 245)"},
+ {authored: "lawngreen", name: "lawngreen", hex: "#7CFC00", hsl: "hsl(90, 100%, 49%)", rgb: "rgb(124, 252, 0)"},
+ {authored: "lemonchiffon", name: "lemonchiffon", hex: "#FFFACD", hsl: "hsl(54, 100%, 90%)", rgb: "rgb(255, 250, 205)"},
+ {authored: "lightblue", name: "lightblue", hex: "#ADD8E6", hsl: "hsl(195, 53%, 79%)", rgb: "rgb(173, 216, 230)"},
+ {authored: "lightcoral", name: "lightcoral", hex: "#F08080", hsl: "hsl(0, 79%, 72%)", rgb: "rgb(240, 128, 128)"},
+ {authored: "lightcyan", name: "lightcyan", hex: "#E0FFFF", hsl: "hsl(180, 100%, 94%)", rgb: "rgb(224, 255, 255)"},
+ {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#FAFAD2", hsl: "hsl(60, 80%, 90%)", rgb: "rgb(250, 250, 210)"},
+ {authored: "lightgray", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
+ {authored: "lightgreen", name: "lightgreen", hex: "#90EE90", hsl: "hsl(120, 73%, 75%)", rgb: "rgb(144, 238, 144)"},
+ {authored: "lightgrey", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
+ {authored: "lightpink", name: "lightpink", hex: "#FFB6C1", hsl: "hsl(351, 100%, 86%)", rgb: "rgb(255, 182, 193)"},
+ {authored: "lightsalmon", name: "lightsalmon", hex: "#FFA07A", hsl: "hsl(17, 100%, 74%)", rgb: "rgb(255, 160, 122)"},
+ {authored: "lightseagreen", name: "lightseagreen", hex: "#20B2AA", hsl: "hsl(177, 70%, 41%)", rgb: "rgb(32, 178, 170)"},
+ {authored: "lightskyblue", name: "lightskyblue", hex: "#87CEFA", hsl: "hsl(203, 92%, 75%)", rgb: "rgb(135, 206, 250)"},
+ {authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
+ {authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
+ {authored: "lightsteelblue", name: "lightsteelblue", hex: "#B0C4DE", hsl: "hsl(214, 41%, 78%)", rgb: "rgb(176, 196, 222)"},
+ {authored: "lightyellow", name: "lightyellow", hex: "#FFFFE0", hsl: "hsl(60, 100%, 94%)", rgb: "rgb(255, 255, 224)"},
+ {authored: "lime", name: "lime", hex: "#0F0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)"},
+ {authored: "limegreen", name: "limegreen", hex: "#32CD32", hsl: "hsl(120, 61%, 50%)", rgb: "rgb(50, 205, 50)"},
+ {authored: "linen", name: "linen", hex: "#FAF0E6", hsl: "hsl(30, 67%, 94%)", rgb: "rgb(250, 240, 230)"},
+ {authored: "magenta", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
+ {authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25%)", rgb: "rgb(128, 0, 0)"},
+ {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66CDAA", hsl: "hsl(160, 51%, 60%)", rgb: "rgb(102, 205, 170)"},
+ {authored: "mediumblue", name: "mediumblue", hex: "#0000CD", hsl: "hsl(240, 100%, 40%)", rgb: "rgb(0, 0, 205)"},
+ {authored: "mediumorchid", name: "mediumorchid", hex: "#BA55D3", hsl: "hsl(288, 59%, 58%)", rgb: "rgb(186, 85, 211)"},
+ {authored: "mediumpurple", name: "mediumpurple", hex: "#9370DB", hsl: "hsl(260, 60%, 65%)", rgb: "rgb(147, 112, 219)"},
+ {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3CB371", hsl: "hsl(147, 50%, 47%)", rgb: "rgb(60, 179, 113)"},
+ {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7B68EE", hsl: "hsl(249, 80%, 67%)", rgb: "rgb(123, 104, 238)"},
+ {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00FA9A", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)"},
+ {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48D1CC", hsl: "hsl(178, 60%, 55%)", rgb: "rgb(72, 209, 204)"},
+ {authored: "mediumvioletred", name: "mediumvioletred", hex: "#C71585", hsl: "hsl(322, 81%, 43%)", rgb: "rgb(199, 21, 133)"},
+ {authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 64%, 27%)", rgb: "rgb(25, 25, 112)"},
+ {authored: "mintcream", name: "mintcream", hex: "#F5FFFA", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)"},
+ {authored: "mistyrose", name: "mistyrose", hex: "#FFE4E1", hsl: "hsl(6, 100%, 94%)", rgb: "rgb(255, 228, 225)"},
+ {authored: "moccasin", name: "moccasin", hex: "#FFE4B5", hsl: "hsl(38, 100%, 85%)", rgb: "rgb(255, 228, 181)"},
+ {authored: "navajowhite", name: "navajowhite", hex: "#FFDEAD", hsl: "hsl(36, 100%, 84%)", rgb: "rgb(255, 222, 173)"},
+ {authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25%)", rgb: "rgb(0, 0, 128)"},
+ {authored: "oldlace", name: "oldlace", hex: "#FDF5E6", hsl: "hsl(39, 85%, 95%)", rgb: "rgb(253, 245, 230)"},
+ {authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25%)", rgb: "rgb(128, 128, 0)"},
+ {authored: "olivedrab", name: "olivedrab", hex: "#6B8E23", hsl: "hsl(80, 60%, 35%)", rgb: "rgb(107, 142, 35)"},
+ {authored: "orange", name: "orange", hex: "#FFA500", hsl: "hsl(39, 100%, 50%)", rgb: "rgb(255, 165, 0)"},
+ {authored: "orangered", name: "orangered", hex: "#FF4500", hsl: "hsl(16, 100%, 50%)", rgb: "rgb(255, 69, 0)"},
+ {authored: "orchid", name: "orchid", hex: "#DA70D6", hsl: "hsl(302, 59%, 65%)", rgb: "rgb(218, 112, 214)"},
+ {authored: "palegoldenrod", name: "palegoldenrod", hex: "#EEE8AA", hsl: "hsl(55, 67%, 80%)", rgb: "rgb(238, 232, 170)"},
+ {authored: "palegreen", name: "palegreen", hex: "#98FB98", hsl: "hsl(120, 93%, 79%)", rgb: "rgb(152, 251, 152)"},
+ {authored: "paleturquoise", name: "paleturquoise", hex: "#AFEEEE", hsl: "hsl(180, 65%, 81%)", rgb: "rgb(175, 238, 238)"},
+ {authored: "palevioletred", name: "palevioletred", hex: "#DB7093", hsl: "hsl(340, 60%, 65%)", rgb: "rgb(219, 112, 147)"},
+ {authored: "papayawhip", name: "papayawhip", hex: "#FFEFD5", hsl: "hsl(37, 100%, 92%)", rgb: "rgb(255, 239, 213)"},
+ {authored: "peachpuff", name: "peachpuff", hex: "#FFDAB9", hsl: "hsl(28, 100%, 86%)", rgb: "rgb(255, 218, 185)"},
+ {authored: "peru", name: "peru", hex: "#CD853F", hsl: "hsl(30, 59%, 53%)", rgb: "rgb(205, 133, 63)"},
+ {authored: "pink", name: "pink", hex: "#FFC0CB", hsl: "hsl(350, 100%, 88%)", rgb: "rgb(255, 192, 203)"},
+ {authored: "plum", name: "plum", hex: "#DDA0DD", hsl: "hsl(300, 47%, 75%)", rgb: "rgb(221, 160, 221)"},
+ {authored: "powderblue", name: "powderblue", hex: "#B0E0E6", hsl: "hsl(187, 52%, 80%)", rgb: "rgb(176, 224, 230)"},
+ {authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25%)", rgb: "rgb(128, 0, 128)"},
+ {authored: "rebeccapurple", name: "rebeccapurple", hex: "#639", hsl: "hsl(270, 50%, 40%)", rgb: "rgb(102, 51, 153)"},
+ {authored: "red", name: "red", hex: "#F00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)"},
+ {authored: "rosybrown", name: "rosybrown", hex: "#BC8F8F", hsl: "hsl(0, 25%, 65%)", rgb: "rgb(188, 143, 143)"},
+ {authored: "royalblue", name: "royalblue", hex: "#4169E1", hsl: "hsl(225, 73%, 57%)", rgb: "rgb(65, 105, 225)"},
+ {authored: "saddlebrown", name: "saddlebrown", hex: "#8B4513", hsl: "hsl(25, 76%, 31%)", rgb: "rgb(139, 69, 19)"},
+ {authored: "salmon", name: "salmon", hex: "#FA8072", hsl: "hsl(6, 93%, 71%)", rgb: "rgb(250, 128, 114)"},
+ {authored: "sandybrown", name: "sandybrown", hex: "#F4A460", hsl: "hsl(28, 87%, 67%)", rgb: "rgb(244, 164, 96)"},
+ {authored: "seagreen", name: "seagreen", hex: "#2E8B57", hsl: "hsl(146, 50%, 36%)", rgb: "rgb(46, 139, 87)"},
+ {authored: "seashell", name: "seashell", hex: "#FFF5EE", hsl: "hsl(25, 100%, 97%)", rgb: "rgb(255, 245, 238)"},
+ {authored: "sienna", name: "sienna", hex: "#A0522D", hsl: "hsl(19, 56%, 40%)", rgb: "rgb(160, 82, 45)"},
+ {authored: "silver", name: "silver", hex: "#C0C0C0", hsl: "hsl(0, 0%, 75%)", rgb: "rgb(192, 192, 192)"},
+ {authored: "skyblue", name: "skyblue", hex: "#87CEEB", hsl: "hsl(197, 71%, 73%)", rgb: "rgb(135, 206, 235)"},
+ {authored: "slateblue", name: "slateblue", hex: "#6A5ACD", hsl: "hsl(248, 53%, 58%)", rgb: "rgb(106, 90, 205)"},
+ {authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
+ {authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
+ {authored: "snow", name: "snow", hex: "#FFFAFA", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)"},
+ {authored: "springgreen", name: "springgreen", hex: "#00FF7F", hsl: "hsl(150, 100%, 50%)", rgb: "rgb(0, 255, 127)"},
+ {authored: "steelblue", name: "steelblue", hex: "#4682B4", hsl: "hsl(207, 44%, 49%)", rgb: "rgb(70, 130, 180)"},
+ {authored: "tan", name: "tan", hex: "#D2B48C", hsl: "hsl(34, 44%, 69%)", rgb: "rgb(210, 180, 140)"},
+ {authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25%)", rgb: "rgb(0, 128, 128)"},
+ {authored: "thistle", name: "thistle", hex: "#D8BFD8", hsl: "hsl(300, 24%, 80%)", rgb: "rgb(216, 191, 216)"},
+ {authored: "tomato", name: "tomato", hex: "#FF6347", hsl: "hsl(9, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
+ {authored: "turquoise", name: "turquoise", hex: "#40E0D0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
+ {authored: "violet", name: "violet", hex: "#EE82EE", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
+ {authored: "wheat", name: "wheat", hex: "#F5DEB3", hsl: "hsl(39, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
+ {authored: "white", name: "white", hex: "#FFF", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
+ {authored: "whitesmoke", name: "whitesmoke", hex: "#F5F5F5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
+ {authored: "yellow", name: "yellow", hex: "#FF0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
+ {authored: "yellowgreen", name: "yellowgreen", hex: "#9ACD32", hsl: "hsl(80, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
+ {authored: "rgba(0, 0, 0, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
+ {authored: "hsla(0, 0%, 0%, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
+ {authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
+ {authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
+ {authored: "rgba(255, 255, 255, 0.6)", name: "rgba(255, 255, 255, 0.6)", hex: "rgba(255, 255, 255, 0.6)", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)"},
+ {authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
+ {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
+ {authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor"},
+ {authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit"},
+ {authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial"},
+ {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""},
+ {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
+ {authored: "unset", name: "unset", hex: "unset", hsl: "unset", rgb: "unset"}
+ ];
+}
diff --git a/toolkit/devtools/shared/test/browser_cubic-bezier-01.js b/toolkit/devtools/shared/test/browser_cubic-bezier-01.js
new file mode 100644
index 000000000..2e288c0ea
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_cubic-bezier-01.js
@@ -0,0 +1,37 @@
+/* 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";
+
+// Tests that the CubicBezierWidget generates content in a given parent node
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Checking that the markup is created in the parent");
+ let container = doc.querySelector("#container");
+ let w = new CubicBezierWidget(container);
+
+ ok(container.querySelector(".coordinate-plane"),
+ "The coordinate plane has been added");
+ let buttons = container.querySelectorAll("button");
+ is(buttons.length, 2,
+ "The 2 control points have been added");
+ is(buttons[0].className, "control-point");
+ is(buttons[0].id, "P1");
+ is(buttons[1].className, "control-point");
+ is(buttons[1].id, "P2");
+ ok(container.querySelector("canvas"), "The curve canvas has been added");
+
+ info("Destroying the widget");
+ w.destroy();
+ is(container.children.length, 0, "All nodes have been removed");
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_cubic-bezier-02.js b/toolkit/devtools/shared/test/browser_cubic-bezier-02.js
new file mode 100644
index 000000000..30887a74d
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_cubic-bezier-02.js
@@ -0,0 +1,149 @@
+/* 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";
+
+// Tests the CubicBezierWidget events
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget, PREDEFINED} =
+ devtools.require("devtools/shared/widgets/CubicBezierWidget");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ yield pointsCanBeDragged(w, win, doc);
+ yield curveCanBeClicked(w, win, doc);
+ yield pointsCanBeMovedWithKeyboard(w, win, doc);
+
+ w.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function* pointsCanBeDragged(widget, win, doc) {
+ info("Checking that the control points can be dragged with the mouse");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P1");
+ widget._onPointMouseDown({target: widget.p1});
+ doc.onmousemove({pageX: 0, pageY: 100});
+ doc.onmouseup();
+
+ let bezier = yield onUpdated;
+ ok(true, "The widget fired the updated event");
+ ok(bezier, "The updated event contains a bezier argument");
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Generating a mousedown/move/up on P2");
+ widget._onPointMouseDown({target: widget.p2});
+ doc.onmousemove({pageX: 200, pageY: 300});
+ doc.onmouseup();
+
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* curveCanBeClicked(widget, win, doc) {
+ info("Checking that clicking on the curve moves the closest control point");
+
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Click close to P1");
+ widget._onCurveClick({pageX: 50, pageY: 150});
+
+ let bezier = yield onUpdated;
+ ok(true, "The widget fired the updated event");
+ is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
+ is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
+
+ info("Listening for the update event");
+ onUpdated = widget.once("updated");
+
+ info("Click close to P2");
+ widget._onCurveClick({pageX: 150, pageY: 250});
+
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+ is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
+ is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
+}
+
+function* pointsCanBeMovedWithKeyboard(widget, win, doc) {
+ info("Checking that points respond to keyboard events");
+
+ info("Moving P1 to the left");
+ let onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
+ let bezier = yield onUpdated;
+ is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the left, fast");
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the right, fast");
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom");
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the bottom, fast");
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
+
+ info("Moving P1 to the top, fast");
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
+ bezier = yield onUpdated;
+ is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+
+ info("Checking that keyboard events also work with P2");
+ info("Moving P2 to the left");
+ onUpdated = widget.once("updated");
+ widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
+ bezier = yield onUpdated;
+ is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
+}
+
+function getKeyEvent(target, keyCode, shift=false) {
+ return {
+ target: target,
+ keyCode: keyCode,
+ shiftKey: shift,
+ preventDefault: () => {}
+ };
+}
diff --git a/toolkit/devtools/shared/test/browser_cubic-bezier-03.js b/toolkit/devtools/shared/test/browser_cubic-bezier-03.js
new file mode 100644
index 000000000..2ce5fe456
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_cubic-bezier-03.js
@@ -0,0 +1,68 @@
+/* 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";
+
+// Tests that coordinates can be changed programatically in the CubicBezierWidget
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget, PREDEFINED} =
+ devtools.require("devtools/shared/widgets/CubicBezierWidget");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let container = doc.querySelector("#container");
+ let w = new CubicBezierWidget(container, PREDEFINED.linear);
+
+ yield coordinatesCanBeChangedByProvidingAnArray(w);
+ yield coordinatesCanBeChangedByProvidingAValue(w);
+
+ w.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function* coordinatesCanBeChangedByProvidingAnArray(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting new coordinates");
+ widget.coordinates = [0,1,1,0];
+
+ let bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting coordinates");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
+}
+
+function* coordinatesCanBeChangedByProvidingAValue(widget) {
+ info("Listening for the update event");
+ let onUpdated = widget.once("updated");
+
+ info("Setting linear css value");
+ widget.cssCubicBezierValue = "linear";
+ let bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], 0, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1, "The new P2 progress coordinate is correct");
+
+ info("Setting a custom cubic-bezier css value");
+ onUpdated = widget.once("updated");
+ widget.cssCubicBezierValue = "cubic-bezier(.25,-0.5, 1, 1.45)";
+ bezier = yield onUpdated;
+ ok(true, "The updated event was fired as a result of setting cssValue");
+
+ is(bezier.P1[0], .25, "The new P1 time coordinate is correct");
+ is(bezier.P1[1], -.5, "The new P1 progress coordinate is correct");
+ is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
+ is(bezier.P2[1], 1.45, "The new P2 progress coordinate is correct");
+}
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-01.js b/toolkit/devtools/shared/test/browser_flame-graph-01.js
new file mode 100644
index 000000000..733841b9e
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that flame graph widget works properly.
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+
+ let readyEventEmitted;
+ graph.once("ready", () => readyEventEmitted = true);
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("flame-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("flame-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+}
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-02.js b/toolkit/devtools/shared/test/browser_flame-graph-02.js
new file mode 100644
index 000000000..ce01e4354
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-02.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that flame graph widgets may have a fixed width or height.
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-03a.js b/toolkit/devtools/shared/test/browser_flame-graph-03a.js
new file mode 100644
index 000000000..2913f60dd
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-03a.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that selections in the flame graph widget work properly.
+
+let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
+let TEST_BOUNDS = { startTime: 0, endTime: 150 };
+let TEST_WIDTH = 200;
+let TEST_HEIGHT = 100;
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, 1);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct (1).");
+
+ scroll(graph, 200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 75,
+ "The selection start boundary is correct (2).");
+ is(graph.getViewRange().endTime | 0, 150,
+ "The selection end boundary is correct (2).");
+
+ scroll(graph, -200, HORIZONTAL_AXIS, 10);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (3).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (3).");
+
+ scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 34,
+ "The selection start boundary is correct (4).");
+ is(graph.getViewRange().endTime | 0, 115,
+ "The selection end boundary is correct (4).");
+
+ scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (5).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (5).");
+
+ dragStart(graph, TEST_WIDTH / 2);
+ is(graph.getViewRange().startTime | 0, 37,
+ "The selection start boundary is correct (6).");
+ is(graph.getViewRange().endTime | 0, 112,
+ "The selection end boundary is correct (6).");
+
+ hover(graph, TEST_WIDTH / 2 - 10);
+ is(graph.getViewRange().startTime | 0, 41,
+ "The selection start boundary is correct (7).");
+ is(graph.getViewRange().endTime | 0, 116,
+ "The selection end boundary is correct (7).");
+
+ dragStop(graph, 10);
+ is(graph.getViewRange().startTime | 0, 71,
+ "The selection start boundary is correct (8).");
+ is(graph.getViewRange().endTime | 0, 145,
+ "The selection end boundary is correct (8).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+let HORIZONTAL_AXIS = 1;
+let VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-03b.js b/toolkit/devtools/shared/test/browser_flame-graph-03b.js
new file mode 100644
index 000000000..707ed9ec2
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-03b.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that selections in the flame graph widget work properly on HiDPI.
+
+let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
+let TEST_BOUNDS = { startTime: 0, endTime: 150 };
+let TEST_WIDTH = 200;
+let TEST_HEIGHT = 100;
+let TEST_DPI_DENSITIY = 2;
+
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
+ graph.fixedWidth = TEST_WIDTH;
+ graph.fixedHeight = TEST_HEIGHT;
+
+ yield graph.ready();
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData({ data: TEST_DATA, bounds: TEST_BOUNDS });
+
+ is(graph.getViewRange().startTime, 0,
+ "The selection start boundary is correct on HiDPI (1).");
+ is(graph.getViewRange().endTime, 150,
+ "The selection end boundary is correct on HiDPI (1).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (1).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (1).");
+
+ scroll(graph, 10000, HORIZONTAL_AXIS, 1);
+
+ is(Math.round(graph.getViewRange().startTime), 150,
+ "The selection start boundary is correct on HiDPI (2).");
+ is(Math.round(graph.getViewRange().endTime), 150,
+ "The selection end boundary is correct on HiDPI (2).");
+
+ is(graph.getOuterBounds().startTime, 0,
+ "The bounds start boundary is correct on HiDPI (2).");
+ is(graph.getOuterBounds().endTime, 150,
+ "The bounds end boundary is correct on HiDPI (2).");
+}
+
+// EventUtils just doesn't work!
+
+let HORIZONTAL_AXIS = 1;
+let VERTICAL_AXIS = 2;
+
+function scroll(graph, wheel, axis, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis,
+ HORIZONTAL_AXIS,
+ VERTICAL_AXIS
+ });
+}
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-04.js b/toolkit/devtools/shared/test/browser_flame-graph-04.js
new file mode 100644
index 000000000..44cdf4e03
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-04.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that text metrics in the flame graph widget work properly.
+
+let HTML_NS = "http://www.w3.org/1999/xhtml";
+let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 8; // px
+let FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
+let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+let L10N = new ViewHelpers.L10N();
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new FlameGraph(doc.body, 1);
+ yield graph.ready();
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ is(graph._averageCharWidth, getAverageCharWidth(),
+ "The average char width was calculated correctly.");
+ is(graph._overflowCharWidth, getCharWidth(L10N.ellipsis),
+ "The ellipsis char width was calculated correctly.");
+
+ let text = "This text is maybe overflowing";
+ let text1000px = graph._getFittedText(text, 1000);
+ let text50px = graph._getFittedText(text, 50);
+ let text10px = graph._getFittedText(text, 10);
+ let text1px = graph._getFittedText(text, 1);
+
+ is(graph._getTextWidthApprox(text), getAverageCharWidth() * text.length,
+ "The approximate width was calculated correctly.");
+
+ info("Text at 1000px width: " + text1000px);
+ info("Text at 50px width : " + text50px);
+ info("Text at 10px width : " + text10px);
+ info("Text at 1px width : " + text1px);
+
+ is(text1000px, text,
+ "The fitted text for 1000px width is correct.");
+
+ isnot(text50px, text,
+ "The fitted text for 50px width is correct (1).");
+
+ ok(text50px.contains(L10N.ellipsis),
+ "The fitted text for 50px width is correct (2).");
+
+ is(graph._getFittedText(text, 10), L10N.ellipsis,
+ "The fitted text for 10px width is correct.");
+
+ is(graph._getFittedText(text, 1), "",
+ "The fitted text for 1px width is correct.");
+}
+
+function getAverageCharWidth() {
+ let letterWidthsSum = 0;
+ let start = 32; // space
+ let end = 123; // "z"
+
+ for (let i = start; i < end; i++) {
+ let char = String.fromCharCode(i);
+ letterWidthsSum += getCharWidth(char);
+ }
+
+ return letterWidthsSum / (end - start);
+}
+
+function getCharWidth(char) {
+ let canvas = document.createElementNS(HTML_NS, "canvas");
+ let ctx = canvas.getContext("2d");
+
+ let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
+ let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
+ ctx.font = fontSize + "px " + fontFamily;
+
+ return ctx.measureText(char).width;
+}
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-utils-01.js b/toolkit/devtools/shared/test/browser_flame-graph-utils-01.js
new file mode 100644
index 000000000..e31341d59
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-utils-01.js
@@ -0,0 +1,261 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that text metrics and data conversion from profiler samples
+// widget work properly in the flame graph.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, 10, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+let TEST_DATA = [{
+ frames: [{
+ location: "M"
+ }, {
+ location: "N",
+ }, {
+ location: "P"
+ }],
+ time: 50,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 100,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "D"
+ }],
+ time: 210,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "E",
+ }, {
+ location: "F"
+ }],
+ time: 330,
+}, {
+ frames: [{
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 460,
+}, {
+ frames: [{
+ location: "X"
+ }, {
+ location: "Y",
+ }, {
+ location: "Z"
+ }],
+ time: 500
+}];
+
+let EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 50,
+ rawLocation: "A"
+ },
+ x: 50,
+ y: 0,
+ width: 410,
+ height: 11,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 50,
+ rawLocation: "B"
+ },
+ x: 50,
+ y: 11,
+ width: 160,
+ height: 11,
+ text: "B"
+ }, {
+ srcData: {
+ startTime: 330,
+ rawLocation: "B"
+ },
+ x: 330,
+ y: 11,
+ width: 130,
+ height: 11,
+ text: "B"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "M"
+ },
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 11,
+ text: "M"
+ }, {
+ srcData: {
+ startTime: 50,
+ rawLocation: "C"
+ },
+ x: 50,
+ y: 22,
+ width: 50,
+ height: 11,
+ text: "C"
+ }, {
+ srcData: {
+ startTime: 330,
+ rawLocation: "C"
+ },
+ x: 330,
+ y: 22,
+ width: 130,
+ height: 11,
+ text: "C"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "N"
+ },
+ x: 0,
+ y: 11,
+ width: 50,
+ height: 11,
+ text: "N"
+ }, {
+ srcData: {
+ startTime: 100,
+ rawLocation: "D"
+ },
+ x: 100,
+ y: 22,
+ width: 110,
+ height: 11,
+ text: "D"
+ }, {
+ srcData: {
+ startTime: 460,
+ rawLocation: "X"
+ },
+ x: 460,
+ y: 0,
+ width: 40,
+ height: 11,
+ text: "X"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 210,
+ rawLocation: "E"
+ },
+ x: 210,
+ y: 11,
+ width: 120,
+ height: 11,
+ text: "E"
+ }, {
+ srcData: {
+ startTime: 460,
+ rawLocation: "Y"
+ },
+ x: 460,
+ y: 11,
+ width: 40,
+ height: 11,
+ text: "Y"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "P"
+ },
+ x: 0,
+ y: 22,
+ width: 50,
+ height: 11,
+ text: "P"
+ }, {
+ srcData: {
+ startTime: 210,
+ rawLocation: "F"
+ },
+ x: 210,
+ y: 22,
+ width: 120,
+ height: 11,
+ text: "F"
+ }, {
+ srcData: {
+ startTime: 460,
+ rawLocation: "Z"
+ },
+ x: 460,
+ y: 22,
+ width: 40,
+ height: 11,
+ text: "Z"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-utils-02.js b/toolkit/devtools/shared/test/browser_flame-graph-utils-02.js
new file mode 100644
index 000000000..2104b87b0
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-utils-02.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests consecutive duplicate frames are removed from the flame graph data.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
+ flattenRecursion: true
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, 10, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+let TEST_DATA = [{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}];
+
+let EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "A"
+ },
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 11,
+ text: "A"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "B"
+ },
+ x: 0,
+ y: 11,
+ width: 50,
+ height: 11,
+ text: "B"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}];
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-utils-03.js b/toolkit/devtools/shared/test/browser_flame-graph-utils-03.js
new file mode 100644
index 000000000..562236d11
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-utils-03.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if platform frames are removed from the flame graph data.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FrameNode} = devtools.require("devtools/shared/profiler/tree-model");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
+ filterFrames: FrameNode.isContent
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, 10, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+let TEST_DATA = [{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50,
+}];
+
+let EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "http://A"
+ },
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 11,
+ text: "http://A"
+ }, {
+ srcData: {
+ startTime: 0,
+ rawLocation: "file://C"
+ },
+ x: 0,
+ y: 22,
+ width: 50,
+ height: 11,
+ text: "file://C"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "https://B"
+ },
+ x: 0,
+ y: 11,
+ width: 50,
+ height: 11,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}];
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-utils-04.js b/toolkit/devtools/shared/test/browser_flame-graph-utils-04.js
new file mode 100644
index 000000000..907f85c4c
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-utils-04.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+let {FrameNode} = devtools.require("devtools/shared/profiler/tree-model");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
+ flattenRecursion: true,
+ filterFrames: FrameNode.isContent,
+ showIdleBlocks: "\m/"
+ });
+
+ ok(out, "Some data was outputted properly");
+ is(out.length, 10, "The outputted length is correct.");
+
+ info("Got flame graph data:\n" + out.toSource() + "\n");
+
+ for (let i = 0; i < out.length; i++) {
+ let found = out[i];
+ let expected = EXPECTED_OUTPUT[i];
+
+ is(found.blocks.length, expected.blocks.length,
+ "The correct number of blocks were found in this bucket.");
+
+ for (let j = 0; j < found.blocks.length; j++) {
+ is(found.blocks[j].x, expected.blocks[j].x,
+ "The expected block X position is correct for this frame.");
+ is(found.blocks[j].y, expected.blocks[j].y,
+ "The expected block Y position is correct for this frame.");
+ is(found.blocks[j].width, expected.blocks[j].width,
+ "The expected block width is correct for this frame.");
+ is(found.blocks[j].height, expected.blocks[j].height,
+ "The expected block height is correct for this frame.");
+ is(found.blocks[j].text, expected.blocks[j].text,
+ "The expected block text is correct for this frame.");
+ }
+ }
+}
+
+let TEST_DATA = [{
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }, {
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 50
+}, {
+ frames: [{
+ location: "chrome://D"
+ }, {
+ location: "resource://E"
+ }],
+ time: 100
+}, {
+ frames: [{
+ location: "http://A"
+ }, {
+ location: "https://B"
+ }, {
+ location: "file://C",
+ }],
+ time: 150
+}];
+
+let EXPECTED_OUTPUT = [{
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "http://A"
+ },
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 11,
+ text: "http://A"
+ }, {
+ srcData: {
+ startTime: 0,
+ rawLocation: "file://C"
+ },
+ x: 0,
+ y: 22,
+ width: 50,
+ height: 11,
+ text: "file://C"
+ }, {
+ srcData: {
+ startTime: 100,
+ rawLocation: "http://A"
+ },
+ x: 100,
+ y: 0,
+ width: 50,
+ height: 11,
+ text: "http://A"
+ }]
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 50,
+ rawLocation: "\m/"
+ },
+ x: 50,
+ y: 0,
+ width: 50,
+ height: 11,
+ text: "\m/"
+ }]
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: []
+}, {
+ blocks: [{
+ srcData: {
+ startTime: 0,
+ rawLocation: "https://B"
+ },
+ x: 0,
+ y: 11,
+ width: 50,
+ height: 11,
+ text: "https://B"
+ }, {
+ srcData: {
+ startTime: 100,
+ rawLocation: "https://B"
+ },
+ x: 100,
+ y: 11,
+ width: 50,
+ height: 11,
+ text: "https://B"
+ }]
+}, {
+ blocks: []
+}];
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-utils-05.js b/toolkit/devtools/shared/test/browser_flame-graph-utils-05.js
new file mode 100644
index 000000000..ca65f253a
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-utils-05.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that flame graph data is cached, and that the cache may be cleared.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let out1 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+ let out2 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+ is(out1, out2, "The outputted data is identical.")
+
+ let out3 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, { flattenRecursion: true });
+ is(out2, out3, "The outputted data is still identical.");
+
+ FlameGraphUtils.removeFromCache(TEST_DATA);
+ let out4 = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, { flattenRecursion: true });
+ isnot(out3, out4, "The outputted data is not identical anymore.");
+}
+
+let TEST_DATA = [{
+ frames: [{
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "A"
+ }, {
+ location: "B",
+ }, {
+ location: "B",
+ }, {
+ location: "C"
+ }],
+ time: 50,
+}];
diff --git a/toolkit/devtools/shared/test/browser_flame-graph-utils-hash.js b/toolkit/devtools/shared/test/browser_flame-graph-utils-hash.js
new file mode 100644
index 000000000..e5509e482
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_flame-graph-utils-hash.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if (idle) nodes are added when necessary in the flame graph data.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+let test = Task.async(function*() {
+ let hash1 = FlameGraphUtils._getStringHash("abc");
+ let hash2 = FlameGraphUtils._getStringHash("acb");
+ let hash3 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("a"));
+ let hash4 = FlameGraphUtils._getStringHash(Array.from(Array(100000)).join("b"));
+
+ isnot(hash1, hash2, "The hashes should not be equal (1).");
+ isnot(hash2, hash3, "The hashes should not be equal (2).");
+ isnot(hash3, hash4, "The hashes should not be equal (3).");
+
+ ok(Number.isInteger(hash1), "The hashes should be integers, not Infinity or NaN (1).");
+ ok(Number.isInteger(hash2), "The hashes should be integers, not Infinity or NaN (2).");
+ ok(Number.isInteger(hash3), "The hashes should be integers, not Infinity or NaN (3).");
+ ok(Number.isInteger(hash4), "The hashes should be integers, not Infinity or NaN (4).");
+
+ finish();
+});
diff --git a/toolkit/devtools/shared/test/browser_graphs-01.js b/toolkit/devtools/shared/test/browser_graphs-01.js
new file mode 100644
index 000000000..3a4555d1c
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-01.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets works properly.
+
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ let readyEventEmitted;
+ graph.once("ready", () => readyEventEmitted = true);
+
+ yield graph.ready();
+ ok(readyEventEmitted, "The 'ready' event should have been emitted");
+
+ testGraph(host, graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ ok(graph._container.classList.contains("line-graph-widget-container"),
+ "The correct graph container was created.");
+ ok(graph._canvas.classList.contains("line-graph-widget-canvas"),
+ "The correct graph container was created.");
+
+ let bounds = host.frame.getBoundingClientRect();
+
+ is(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph has the correct height.");
+
+ ok(graph._cursor.x === null,
+ "The graph's cursor X coordinate is initially null.");
+ ok(graph._cursor.y === null,
+ "The graph's cursor Y coordinate is initially null.");
+
+ ok(graph._selection.start === null,
+ "The graph's selection start value is initially null.");
+ ok(graph._selection.end === null,
+ "The graph's selection end value is initially null.");
+
+ ok(graph._selectionDragger.origin === null,
+ "The graph's dragger origin value is initially null.");
+ ok(graph._selectionDragger.anchor.start === null,
+ "The graph's dragger anchor start value is initially null.");
+ ok(graph._selectionDragger.anchor.end === null,
+ "The graph's dragger anchor end value is initially null.");
+
+ ok(graph._selectionResizer.margin === null,
+ "The graph's resizer margin value is initially null.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-02.js b/toolkit/devtools/shared/test/browser_graphs-02.js
new file mode 100644
index 000000000..907cb5701
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-02.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets can properly add data, regions and highlights.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testDataAndRegions(graph);
+ testHighlights(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testDataAndRegions(graph) {
+ let thrown1;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown1 = true;
+ }
+ ok(thrown1, "Setting regions before setting data shouldn't work.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ let thrown2;
+ try {
+ graph.setRegions(TEST_REGIONS);
+ } catch (e) {
+ thrown2 = true;
+ }
+ ok(thrown2, "Setting regions twice shouldn't work.");
+
+ ok(graph.hasData(), "The graph should now have the data source set.");
+ ok(graph.hasRegions(), "The graph should now have the regions set.");
+
+ is(graph.dataScaleX,
+ graph.width / 4180, // last & first tick in TEST_DATA
+ "The data scale on the X axis is correct.");
+
+ is(graph.dataScaleY,
+ graph.height / 60 * 0.85, // max value in TEST_DATA * GRAPH_DAMPEN_VALUES
+ "The data scale on the Y axis is correct.");
+
+ for (let i = 0; i < TEST_REGIONS.length; i++) {
+ let original = TEST_REGIONS[i];
+ let normalized = graph._regions[i];
+
+ is(original.start * graph.dataScaleX, normalized.start,
+ "The region's start value was properly normalized.");
+ is(original.end * graph.dataScaleX, normalized.end,
+ "The region's end value was properly normalized.");
+ }
+}
+
+function testHighlights(graph) {
+ graph.setMask(TEST_REGIONS);
+ ok(graph.hasMask(),
+ "The graph should now have the highlights set.");
+
+ graph.setMask([]);
+ ok(graph.hasMask(),
+ "The graph shouldn't have anything highlighted.");
+
+ graph.setMask(null);
+ ok(!graph.hasMask(),
+ "The graph should have everything highlighted.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-03.js b/toolkit/devtools/shared/test/browser_graphs-03.js
new file mode 100644
index 000000000..b97acbffc
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-03.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets can handle clients getting/setting the
+// selection or cursor.
+
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ yield testSelection(graph);
+ yield testCursor(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testSelection(graph) {
+ ok(graph.getSelection().start === null,
+ "The graph's selection should initially have a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection should initially have a null end value.");
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+
+ let selected = graph.once("selecting");
+ graph.setSelection({ start: 100, end: 200 });
+
+ yield selected;
+ ok(true, "A 'selecting' event has been fired.");
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelection().start, 100,
+ "The graph's selection now has an updated start value.");
+ is(graph.getSelection().end, 200,
+ "The graph's selection now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setSelection({ start: null, end: null });
+ } catch(e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null selection shouldn't work.");
+
+ ok(graph.hasSelection(),
+ "There should still be a selection.");
+
+ let deselected = graph.once("deselecting");
+ graph.dropSelection();
+
+ yield deselected;
+ ok(true, "A 'deselecting' event has been fired.");
+
+ ok(!graph.hasSelection(),
+ "There shouldn't be any selection anymore.");
+ ok(graph.getSelection().start === null,
+ "The graph's selection now has a null start value.");
+ ok(graph.getSelection().end === null,
+ "The graph's selection now has a null end value.");
+}
+
+function* testCursor(graph) {
+ ok(graph.getCursor().x === null,
+ "The graph's cursor should initially have a null X value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor should initially have a null Y value.");
+ ok(!graph.hasCursor(),
+ "There shouldn't initially be any cursor.");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.hasCursor(),
+ "There should now be a cursor.");
+ is(graph.getCursor().x, 100,
+ "The graph's cursor now has an updated start value.");
+ is(graph.getCursor().y, 50,
+ "The graph's cursor now has an updated end value.");
+
+ let thrown;
+ try {
+ graph.setCursor({ x: null, y: null });
+ } catch(e) {
+ thrown = true;
+ }
+ ok(thrown, "Setting a null cursor shouldn't work.");
+
+ ok(graph.hasCursor(),
+ "There should still be a cursor.");
+
+ graph.dropCursor();
+
+ ok(!graph.hasCursor(),
+ "There shouldn't be any cursor anymore.");
+ ok(graph.getCursor().x === null,
+ "The graph's cursor now has a null start value.");
+ ok(graph.getCursor().y === null,
+ "The graph's cursor now has a null end value.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-04.js b/toolkit/devtools/shared/test/browser_graphs-04.js
new file mode 100644
index 000000000..2d90b09bf
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-04.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets can correctly compare selections and cursors.
+
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.hasSelection(),
+ "There shouldn't initially be any selection.");
+ is(graph.getSelectionWidth(), 0,
+ "The selection width should be 0 when there's no selection.");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ ok(graph.hasSelection(),
+ "There should now be a selection.");
+ is(graph.getSelectionWidth(), 100,
+ "The selection width should now be 100.");
+
+ ok(graph.isSelectionDifferent({ start: 100, end: 201 }),
+ "The selection was correctly reported to be different (1).");
+ ok(graph.isSelectionDifferent({ start: 101, end: 200 }),
+ "The selection was correctly reported to be different (2).");
+ ok(graph.isSelectionDifferent({ start: null, end: null }),
+ "The selection was correctly reported to be different (3).");
+ ok(graph.isSelectionDifferent(null),
+ "The selection was correctly reported to be different (4).");
+
+ ok(!graph.isSelectionDifferent({ start: 100, end: 200 }),
+ "The selection was incorrectly reported to be different (1).");
+ ok(!graph.isSelectionDifferent(graph.getSelection()),
+ "The selection was incorrectly reported to be different (2).");
+
+ graph.setCursor({ x: 100, y: 50 });
+
+ ok(graph.isCursorDifferent({ x: 100, y: 51 }),
+ "The cursor was correctly reported to be different (1).");
+ ok(graph.isCursorDifferent({ x: 101, y: 50 }),
+ "The cursor was correctly reported to be different (2).");
+ ok(graph.isCursorDifferent({ x: null, y: null }),
+ "The cursor was correctly reported to be different (3).");
+ ok(graph.isCursorDifferent(null),
+ "The cursor was correctly reported to be different (4).");
+
+ ok(!graph.isCursorDifferent({ x: 100, y: 50 }),
+ "The cursor was incorrectly reported to be different (1).");
+ ok(!graph.isCursorDifferent(graph.getCursor()),
+ "The cursor was incorrectly reported to be different (2).");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-05.js b/toolkit/devtools/shared/test/browser_graphs-05.js
new file mode 100644
index 000000000..78fdfbf06
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-05.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets can correctly determine which regions are hovered.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no regions.");
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ ok(!graph.getHoveredRegion(),
+ "There should be no hovered region yet because there's no cursor.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX - 1, y: 0 });
+ ok(!graph.getHoveredRegion(),
+ "There shouldn't be any hovered region yet.");
+
+ graph.setCursor({ x: TEST_REGIONS[0].start * graph.dataScaleX + 1, y: 0 });
+ ok(graph.getHoveredRegion(),
+ "There should be a hovered region now.");
+ is(graph.getHoveredRegion().start, 320 * graph.dataScaleX,
+ "The reported hovered region is correct (1).");
+ is(graph.getHoveredRegion().end, 460 * graph.dataScaleX,
+ "The reported hovered region is correct (2).");
+
+ graph.setSelection({ start: 100, end: 200 });
+
+ info("Setting cursor over the left boundary.");
+ graph.setCursor({ x: 100, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the left boundary.");
+ graph.setCursor({ x: 105, y: 0 });
+
+ ok(graph._isHoveringStartBoundary(),
+ "The graph start boundary should be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the selection.");
+ graph.setCursor({ x: 150, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor near the right boundary.");
+ graph.setCursor({ x: 195, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(graph._isHoveringSelectionContents(),
+ "The graph contents should be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting cursor over the right boundary.");
+ graph.setCursor({ x: 200, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(graph._isHoveringEndBoundary(),
+ "The graph end boundary should be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should be hovered.");
+
+ info("Setting away from the selection.");
+ graph.setCursor({ x: 300, y: 0 });
+
+ ok(!graph._isHoveringStartBoundary(),
+ "The graph start boundary should not be hovered.");
+ ok(!graph._isHoveringEndBoundary(),
+ "The graph end boundary should not be hovered.");
+ ok(!graph._isHoveringSelectionContents(),
+ "The graph contents should not be hovered.");
+ ok(!graph._isHoveringSelectionContentsOrBoundaries(),
+ "The graph contents or boundaries should not be hovered.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-06.js b/toolkit/devtools/shared/test/browser_graphs-06.js
new file mode 100644
index 000000000..781b0ed9c
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-06.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if clicking on regions adds a selection spanning that region.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+const TEST_REGIONS = [{ start: 320, end: 460 }, { start: 780, end: 860 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.setRegions(TEST_REGIONS);
+
+ click(graph, (graph._regions[0].start + graph._regions[0].end) / 2);
+ is(graph.getSelection().start, graph._regions[0].start,
+ "The first region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[0].end,
+ "The first region is now selected (2).");
+
+ let min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ let max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (1).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (2).");
+
+ click(graph, (graph._regions[1].start + graph._regions[1].end) / 2);
+ is(graph.getSelection().start, graph._regions[1].start,
+ "The second region is now selected (1).");
+ is(graph.getSelection().end, graph._regions[1].end,
+ "The second region is now selected (2).");
+
+ min = map(graph.getSelection().start, 0, graph.width, 112, 4180);
+ max = map(graph.getSelection().end, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (3).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (4).");
+
+ graph.setSelection({ start: graph.width, end: 0 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (5).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (6).");
+
+ graph.setSelection({ start: graph.width + 100, end: -100 });
+ min = map(0, 0, graph.width, 112, 4180);
+ max = map(graph.width, 0, graph.width, 112, 4180);
+ is(graph.getMappedSelection().min, min,
+ "The mapped selection's min value is correct (7).");
+ is(graph.getMappedSelection().max, max,
+ "The mapped selection's max value is correct (8).");
+}
+
+/**
+ * Maps a value from one range to another.
+ */
+function map(value, istart, istop, ostart, ostop) {
+ return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-07a.js b/toolkit/devtools/shared/test/browser_graphs-07a.js
new file mode 100644
index 000000000..d0828b2ee
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-07a.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if selecting, resizing, moving selections and zooming in/out works.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+
+ info("Resizing by dragging the end handlebar.");
+
+ dragStart(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (7).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (7).");
+
+ dragStop(graph, 600);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (8).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (8).");
+
+ info("Resizing by dragging the start handlebar.");
+
+ dragStart(graph, 200);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (9).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (9).");
+
+ dragStop(graph, 100);
+ is(graph.getSelection().start, 100,
+ "The current selection start value is correct (10).");
+ is(graph.getSelection().end, 600,
+ "The current selection end value is correct (10).");
+
+ info("Moving by dragging the selection.");
+
+ dragStart(graph, 300);
+ hover(graph, 400);
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (11).");
+ is(graph.getSelection().end, 700,
+ "The current selection end value is correct (11).");
+
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (12).");
+ is(graph.getSelection().end, 800,
+ "The current selection end value is correct (12).");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 600);
+ is(graph.getSelection().start, 525,
+ "The current selection start value is correct (13).");
+ is(graph.getSelection().end, 650,
+ "The current selection end value is correct (13).");
+
+ info("Zooming out by scrolling inside the selection.");
+
+ scroll(graph, 1000, 600);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (14).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (14).");
+
+ info("Sliding left by scrolling outside the selection.");
+
+ scroll(graph, 100, 900);
+ is(graph.getSelection().start, 458.75,
+ "The current selection start value is correct (15).");
+ is(graph.getSelection().end, 677.5,
+ "The current selection end value is correct (15).");
+
+ info("Sliding right by scrolling outside the selection.");
+
+ scroll(graph, -100, 900);
+ is(graph.getSelection().start, 468.75,
+ "The current selection start value is correct (16).");
+ is(graph.getSelection().end, 687.5,
+ "The current selection end value is correct (16).");
+
+ info("Zooming out a lot.");
+
+ scroll(graph, Number.MAX_SAFE_INTEGER, 500);
+ is(graph.getSelection().start, 1,
+ "The current selection start value is correct (17).");
+ is(graph.getSelection().end, graph.width - 1,
+ "The current selection end value is correct (17).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseWheel({ clientX: x, clientY: y, detail: wheel });
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-07b.js b/toolkit/devtools/shared/test/browser_graphs-07b.js
new file mode 100644
index 000000000..fe24cf64f
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-07b.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if selections can't be added via clicking, while not allowed.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+ graph.selectionEnabled = false;
+
+ info("Attempting to make a selection.");
+
+ dragStart(graph, 300);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (1).");
+
+ hover(graph, 400);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (2).");
+
+ dragStop(graph, 500);
+ is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+ "The graph shouldn't have a selection (3).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-08.js b/toolkit/devtools/shared/test/browser_graphs-08.js
new file mode 100644
index 000000000..158e12823
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-08.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if a selection is dropped when clicking outside of it.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.setData(TEST_DATA);
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ ok(graph.hasSelection(),
+ "A selection should be available.");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct.");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct.");
+
+ click(graph, 600);
+ ok(!graph.hasSelection(),
+ "The selection should be dropped.");
+}
+
+// EventUtils just doesn't work!
+
+function click(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-09a.js b/toolkit/devtools/shared/test/browser_graphs-09a.js
new file mode 100644
index 000000000..ff59ce997
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-09a.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs properly create the gutter and tooltips.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, { metric: "fps" });
+
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ info("Should be able to set the graph data before waiting for the ready event.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+ ok(graph.hasData(), "Data was set successfully.");
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden because the tooltips have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "true",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "true",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "true",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
+ "The maximum tooltip displays the correct info.");
+ is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
+ "The average tooltip displays the correct info.");
+ is(graph._minTooltip.querySelector("[text=info]").textContent, "min",
+ "The minimum tooltip displays the correct info.");
+
+ is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
+ "The maximum tooltip displays the correct value.");
+ is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.71",
+ "The average tooltip displays the correct value.");
+ is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
+ "The minimum tooltip displays the correct value.");
+
+ is(graph._maxTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The maximum tooltip displays the correct metric.");
+ is(graph._avgTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The average tooltip displays the correct metric.");
+ is(graph._minTooltip.querySelector("[text=metric]").textContent, "fps",
+ "The minimum tooltip displays the correct metric.");
+
+ is(parseInt(graph._maxTooltip.style.top), 22,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top), 61,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top), 128,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-09b.js b/toolkit/devtools/shared/test/browser_graphs-09b.js
new file mode 100644
index 000000000..0cae0d467
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-09b.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs properly use the tooltips configuration properties.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.withTooltipArrows = false;
+ graph.withFixedTooltipPositions = true;
+
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible even if the tooltips don't have arrows.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should not be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+
+ is(graph._maxTooltip.getAttribute("with-arrows"), "false",
+ "The maximum tooltip has the correct 'with-arrows' attribute.");
+ is(graph._avgTooltip.getAttribute("with-arrows"), "false",
+ "The average tooltip has the correct 'with-arrows' attribute.");
+ is(graph._minTooltip.getAttribute("with-arrows"), "false",
+ "The minimum tooltip has the correct 'with-arrows' attribute.");
+
+ is(parseInt(graph._maxTooltip.style.top), 8,
+ "The maximum tooltip is positioned correctly.");
+ is(parseInt(graph._avgTooltip.style.top), 8,
+ "The average tooltip is positioned correctly.");
+ is(parseInt(graph._minTooltip.style.top), 142,
+ "The minimum tooltip is positioned correctly.");
+
+ is(parseInt(graph._maxGutterLine.style.top), 22,
+ "The maximum gutter line is positioned correctly.");
+ is(parseInt(graph._avgGutterLine.style.top), 61,
+ "The average gutter line is positioned correctly.");
+ is(parseInt(graph._minGutterLine.style.top), 128,
+ "The minimum gutter line is positioned correctly.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-09c.js b/toolkit/devtools/shared/test/browser_graphs-09c.js
new file mode 100644
index 000000000..97e4dd7e5
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-09c.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs hide the tooltips when there's no data available.
+
+const TEST_DATA = [];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden, since there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-09d.js b/toolkit/devtools/shared/test/browser_graphs-09d.js
new file mode 100644
index 000000000..c56163aa6
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-09d.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs hide the 'max' tooltip when the distance between
+// the 'min' and 'max' tooltip is too small.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 59.9 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should not be hidden.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should not be hidden.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should not be hidden.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-09e.js b/toolkit/devtools/shared/test/browser_graphs-09e.js
new file mode 100644
index 000000000..2f75f3099
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-09e.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that line graphs hide the gutter and tooltips when there's no data,
+// but show them when there is.
+
+const NO_DATA = [];
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden when there's no data available.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden when there's no data available.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden when there's no data available.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden when there's no data available.");
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ is(graph._gutter.hidden, false,
+ "The gutter should be visible now.");
+ is(graph._maxTooltip.hidden, false,
+ "The max tooltip should be visible now.");
+ is(graph._avgTooltip.hidden, false,
+ "The avg tooltip should be visible now.");
+ is(graph._minTooltip.hidden, false,
+ "The min tooltip should be visible now.");
+
+ yield graph.setDataWhenReady(NO_DATA);
+
+ is(graph._gutter.hidden, true,
+ "The gutter should be hidden again.");
+ is(graph._maxTooltip.hidden, true,
+ "The max tooltip should be hidden again.");
+ is(graph._avgTooltip.hidden, true,
+ "The avg tooltip should be hidden again.");
+ is(graph._minTooltip.hidden, true,
+ "The min tooltip should be hidden again.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-09f.js b/toolkit/devtools/shared/test/browser_graphs-09f.js
new file mode 100644
index 000000000..7b43a4c41
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-09f.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the constructor options for `min`, `max` and `avg` on displaying the
+// gutter/tooltips and lines.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+
+ yield testGraph(doc.body, { avg: false });
+ yield testGraph(doc.body, { min: false });
+ yield testGraph(doc.body, { max: false });
+ yield testGraph(doc.body, { min: false, max: false, avg: false });
+ yield testGraph(doc.body, {});
+
+ host.destroy();
+}
+
+function* testGraph (parent, options) {
+ options.metric = "fps";
+ let graph = new LineGraphWidget(parent, options);
+ yield graph.setDataWhenReady(TEST_DATA);
+ let shouldGutterShow = options.min === false && options.max === false;
+
+ is(graph._gutter.hidden, shouldGutterShow,
+ `The gutter should ${shouldGutterShow ? "" : "not "}be shown`);
+
+ is(graph._maxTooltip.hidden, options.max === false,
+ `The max tooltip should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._maxGutterLine.hidden, options.max === false,
+ `The max gutter should ${options.max === false ? "not " : ""}be shown`);
+ is(graph._minTooltip.hidden, options.min === false,
+ `The min tooltip should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._minGutterLine.hidden, options.min === false,
+ `The min gutter should ${options.min === false ? "not " : ""}be shown`);
+ is(graph._avgTooltip.hidden, options.avg === false,
+ `The avg tooltip should ${options.avg === false ? "not " : ""}be shown`);
+ is(graph._avgGutterLine.hidden, options.avg === false,
+ `The avg gutter should ${options.avg === false ? "not " : ""}be shown`);
+
+ graph.destroy();
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-10a.js b/toolkit/devtools/shared/test/browser_graphs-10a.js
new file mode 100644
index 000000000..c5f2f8d94
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-10a.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graphs properly handle resizing.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost("window");
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ graph.on("refresh", () => refreshCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 2, "The graph should've been refreshed 2 times.");
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+ let initialBounds = host.frame.getBoundingClientRect();
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh");
+ let newBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newBounds.width, 100,
+ "The window was properly resized (1).");
+ is(initialBounds.height - newBounds.height, 100,
+ "The window was properly resized (2).");
+
+ is(graph.width, newBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (1).");
+ is(graph.height, newBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (1).");
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (1).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (1).");
+
+ hover(graph, 400);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (2).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (2).");
+
+ dragStop(graph, 500);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (3).");
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (3).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (3).");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh");
+ let newerBounds = host.frame.getBoundingClientRect();
+
+ is(initialBounds.width - newerBounds.width, 0,
+ "The window was properly resized (3).");
+ is(initialBounds.height - newerBounds.height, 0,
+ "The window was properly resized (4).");
+
+ is(graph.width, newerBounds.width * window.devicePixelRatio,
+ "The graph has the correct width (2).");
+ is(graph.height, newerBounds.height * window.devicePixelRatio,
+ "The graph has the correct height (2).");
+
+ info("Making a new selection.");
+
+ dragStart(graph, 200);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should start (4).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (4).");
+ is(graph.getSelection().end, 200,
+ "The current selection end value is correct (4).");
+
+ hover(graph, 300);
+ ok(graph.hasSelectionInProgress(),
+ "The selection should still be in progress (5).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (5).");
+ is(graph.getSelection().end, 300,
+ "The current selection end value is correct (5).");
+
+ dragStop(graph, 400);
+ ok(!graph.hasSelectionInProgress(),
+ "The selection should have stopped (6).");
+ is(graph.getSelection().start, 200,
+ "The current selection start value is correct (6).");
+ is(graph.getSelection().end, 400,
+ "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-10b.js b/toolkit/devtools/shared/test/browser_graphs-10b.js
new file mode 100644
index 000000000..ddb0f5a0f
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-10b.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graphs aren't refreshed when the owner window resizes but
+// the graph dimensions stay the same.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost("window");
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+ yield graph.once("ready");
+
+ let refreshCount = 0;
+ let refreshCancelledCount = 0;
+ graph.on("refresh", () => refreshCount++);
+ graph.on("refresh-cancelled", () => refreshCancelledCount++);
+
+ yield testGraph(host, graph);
+
+ is(refreshCount, 0, "The graph shouldn't have been refreshed at all.");
+ is(refreshCancelledCount, 2, "The graph should've had 2 refresh attempts.");
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(host, graph) {
+ graph.setData(TEST_DATA);
+
+ host._window.resizeBy(-100, -100);
+ yield graph.once("refresh-cancelled");
+
+ host._window.resizeBy(100, 100);
+ yield graph.once("refresh-cancelled");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-11a.js b/toolkit/devtools/shared/test/browser_graphs-11a.js
new file mode 100644
index 000000000..51f8b5a02
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-11a.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that bar graph create a legend as expected.
+
+let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new BarGraphWidget(doc.body);
+ yield graph.once("ready");
+
+ testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.setData([{ delta: 0, values: [] }]);
+
+ let legendContainer = graph._document.querySelector(".bar-graph-widget-legend");
+ ok(legendContainer,
+ "A legend container should be available.");
+ is(legendContainer.childNodes.length, 3,
+ "Three legend items should have been created.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ is(legendItems[0].querySelector("[view=color]").style.backgroundColor, "rgb(70, 175, 227)",
+ "The first legend item has the correct color.");
+ is(legendItems[1].querySelector("[view=color]").style.backgroundColor, "rgb(235, 83, 104)",
+ "The second legend item has the correct color.");
+ is(legendItems[2].querySelector("[view=color]").style.backgroundColor, "rgb(112, 191, 83)",
+ "The third legend item has the correct color.");
+
+ is(legendItems[0].querySelector("[view=label]").textContent, "Foo",
+ "The first legend item has the correct label.");
+ is(legendItems[1].querySelector("[view=label]").textContent, "Bar",
+ "The second legend item has the correct label.");
+ is(legendItems[2].querySelector("[view=label]").textContent, "Baz",
+ "The third legend item has the correct label.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-11b.js b/toolkit/devtools/shared/test/browser_graphs-11b.js
new file mode 100644
index 000000000..bf283e2d3
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-11b.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that bar graph's legend items handle mouseover/mouseout.
+
+let {BarGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+const CATEGORIES = [
+ { color: "#46afe3", label: "Foo" },
+ { color: "#eb5368", label: "Bar" },
+ { color: "#70bf53", label: "Baz" }
+];
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new BarGraphWidget(doc.body, 1);
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.once("ready");
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ graph.format = CATEGORIES;
+ graph.dataOffsetX = 1000;
+ graph.setData([{
+ delta: 1100, values: [0, 2, 3]
+ }, {
+ delta: 1200, values: [1, 0, 2]
+ }, {
+ delta: 1300, values: [2, 1, 0]
+ }, {
+ delta: 1400, values: [0, 3, 1]
+ }, {
+ delta: 1500, values: [3, 0, 2]
+ }, {
+ delta: 1600, values: [3, 2, 0]
+ }]);
+
+ is(graph._blocksBoundingRects.toSource(), "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ "The correct blocks bounding rects were calculated for the bar graph.");
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ is(legendItems.length, 3,
+ "Three legend items should exist in the entire graph.");
+
+ yield testLegend(graph, 0, {
+ highlights: "[{type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100}, {type:0, start:67.66666666666666, end:100, top:70, bottom:100}, {type:0, start:134.33333333333331, end:166.66666666666666, top:55, bottom:100}, {type:0, start:167.66666666666666, end:200, top:55, bottom:100}]",
+ selection: "({start:34.33333333333333, end:200})",
+ leftmost: "({type:0, start:34.33333333333333, end:66.66666666666666, top:85, bottom:100})",
+ rightmost: "({type:0, start:167.66666666666666, end:200, top:55, bottom:100})"
+ });
+ yield testLegend(graph, 1, {
+ highlights: "[{type:1, start:0, end:33.33333333333333, top:70, bottom:100}, {type:1, start:67.66666666666666, end:100, top:54, bottom:69}, {type:1, start:101, end:133.33333333333331, top:55, bottom:100}, {type:1, start:167.66666666666666, end:200, top:24, bottom:54}]",
+ selection: "({start:0, end:200})",
+ leftmost: "({type:1, start:0, end:33.33333333333333, top:70, bottom:100})",
+ rightmost: "({type:1, start:167.66666666666666, end:200, top:24, bottom:54})"
+ });
+ yield testLegend(graph, 2, {
+ highlights: "[{type:2, start:0, end:33.33333333333333, top:24, bottom:69}, {type:2, start:34.33333333333333, end:66.66666666666666, top:54, bottom:84}, {type:2, start:101, end:133.33333333333331, top:39, bottom:54}, {type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54}]",
+ selection: "({start:0, end:166.66666666666666})",
+ leftmost: "({type:2, start:0, end:33.33333333333333, top:24, bottom:69})",
+ rightmost: "({type:2, start:134.33333333333331, end:166.66666666666666, top:24, bottom:54})"
+ });
+}
+
+function* testLegend(graph, index, { highlights, selection, leftmost, rightmost }) {
+ // Hover.
+
+ let legendItems = graph._document.querySelectorAll(".bar-graph-widget-legend-item");
+ let colorBlock = legendItems[index].querySelector("[view=color]");
+
+ let debounced = graph.once("legend-hover");
+ graph._onLegendMouseOver({ target: colorBlock });
+ ok(!graph.hasMask(), "The graph shouldn't get highlights immediately.");
+
+ let [type, rects] = yield debounced;
+ ok(graph.hasMask(), "The graph should now have highlights.");
+
+ is(type, index,
+ "The legend item was correctly hovered.");
+ is(rects.toSource(), highlights,
+ "The legend item highlighted the correct regions.");
+
+ // Unhover.
+
+ let unhovered = graph.once("legend-unhover");
+ graph._onLegendMouseOut();
+ ok(!graph.hasMask(), "The graph shouldn't have highlights anymore.");
+
+ yield unhovered;
+ ok(true, "The 'legend-mouseout' event was emitted.");
+
+ // Select.
+
+ let selected = graph.once("legend-selection");
+ graph._onLegendMouseDown(mockEvent(colorBlock));
+ ok(graph.hasSelection(), "The graph should now have a selection.");
+ is(graph.getSelection().toSource(), selection, "The graph has a correct selection.");
+
+ let [left, right] = yield selected;
+ is(left.toSource(), leftmost, "The correct leftmost data block was found.");
+ is(right.toSource(), rightmost, "The correct rightmost data block was found.");
+
+ // Deselect.
+
+ graph.dropSelection();
+}
+
+function mockEvent(node) {
+ return {
+ target: node,
+ preventDefault: () => {},
+ stopPropagation: () => {}
+ };
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-12.js b/toolkit/devtools/shared/test/browser_graphs-12.js
new file mode 100644
index 000000000..1517f3efb
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-12.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that canvas graphs can have their selection linked.
+
+let {LineGraphWidget, BarGraphWidget, CanvasGraphUtils} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let first = document.createElement("div");
+ first.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(first);
+
+ let second = document.createElement("div");
+ second.setAttribute("style", "display: inline-block; width: 100%; height: 50%;");
+ doc.body.appendChild(second);
+
+ let graph1 = new LineGraphWidget(first, "js");
+ let graph2 = new BarGraphWidget(second);
+
+ CanvasGraphUtils.linkAnimation(graph1, graph2);
+ CanvasGraphUtils.linkSelection(graph1, graph2);
+
+ yield graph1.ready();
+ yield graph2.ready();
+
+ testGraphs(graph1, graph2);
+
+ graph1.destroy();
+ graph2.destroy();
+ host.destroy();
+}
+
+function testGraphs(graph1, graph2) {
+ info("Making a selection in the first graph.");
+
+ dragStart(graph1, 300);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should start (1.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not start yet in the second graph (1.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (1.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (1.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (1.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (1.2).");
+
+ hover(graph1, 400);
+ ok(graph1.hasSelectionInProgress(),
+ "The selection should still be in progress (2.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should not be in progress in the second graph (2.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (2.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (2.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (2.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (2.2).");
+
+ dragStop(graph1, 500);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (3.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (3.2).");
+ is(graph1.getSelection().start, 300,
+ "The current selection start value is correct (3.1).");
+ is(graph2.getSelection().start, 300,
+ "The current selection start value is correct (3.2).");
+ is(graph1.getSelection().end, 500,
+ "The current selection end value is correct (3.1).");
+ is(graph2.getSelection().end, 500,
+ "The current selection end value is correct (3.2).");
+
+ info("Making a new selection in the second graph.");
+
+ dragStart(graph2, 200);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not start yet in the first graph (4.1).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should start (4.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (4.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (4.2).");
+ is(graph1.getSelection().end, 200,
+ "The current selection end value is correct (4.1).");
+ is(graph2.getSelection().end, 200,
+ "The current selection end value is correct (4.2).");
+
+ hover(graph2, 300);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should not be in progress in the first graph (2.2).");
+ ok(graph2.hasSelectionInProgress(),
+ "The selection should still be in progress (5.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (5.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (5.2).");
+ is(graph1.getSelection().end, 300,
+ "The current selection end value is correct (5.1).");
+ is(graph2.getSelection().end, 300,
+ "The current selection end value is correct (5.2).");
+
+ dragStop(graph2, 400);
+ ok(!graph1.hasSelectionInProgress(),
+ "The selection should have stopped (6.1).");
+ ok(!graph2.hasSelectionInProgress(),
+ "The selection should have stopped (6.2).");
+ is(graph1.getSelection().start, 200,
+ "The current selection start value is correct (6.1).");
+ is(graph2.getSelection().start, 200,
+ "The current selection start value is correct (6.2).");
+ is(graph1.getSelection().end, 400,
+ "The current selection end value is correct (6.1).");
+ is(graph2.getSelection().end, 400,
+ "The current selection end value is correct (6.2).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-13.js b/toolkit/devtools/shared/test/browser_graphs-13.js
new file mode 100644
index 000000000..f0f09203c
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-13.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets may have a fixed width or height.
+
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
+
+ let graph = new LineGraphWidget(doc.body, "fps");
+ graph.fixedWidth = 200;
+ graph.fixedHeight = 100;
+
+ yield graph.ready();
+ testGraph(host, graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function testGraph(host, graph) {
+ let bounds = host.frame.getBoundingClientRect();
+
+ isnot(graph.width, bounds.width * window.devicePixelRatio,
+ "The graph should not span all the parent node's width.");
+ isnot(graph.height, bounds.height * window.devicePixelRatio,
+ "The graph should not span all the parent node's height.");
+
+ is(graph.width, graph.fixedWidth * window.devicePixelRatio,
+ "The graph has the correct width.");
+ is(graph.height, graph.fixedHeight * window.devicePixelRatio,
+ "The graph has the correct height.");
+}
diff --git a/toolkit/devtools/shared/test/browser_graphs-14.js b/toolkit/devtools/shared/test/browser_graphs-14.js
new file mode 100644
index 000000000..ce7b96a5d
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_graphs-14.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that graph widgets correctly emit mouse input events.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost();
+ let graph = new LineGraphWidget(doc.body, "fps");
+
+ yield testGraph(graph);
+
+ graph.destroy();
+ host.destroy();
+}
+
+function* testGraph(graph) {
+ let mouseDownEvents = 0;
+ let mouseUpEvents = 0;
+ let scrollEvents = 0;
+ graph.on("mousedown", () => mouseDownEvents++);
+ graph.on("mouseup", () => mouseUpEvents++);
+ graph.on("scroll", () => scrollEvents++);
+
+ yield graph.setDataWhenReady(TEST_DATA);
+
+ info("Making a selection.");
+
+ dragStart(graph, 300);
+ dragStop(graph, 500);
+ is(graph.getSelection().start, 300,
+ "The current selection start value is correct (1).");
+ is(graph.getSelection().end, 500,
+ "The current selection end value is correct (1).");
+
+ is(mouseDownEvents, 1,
+ "One mousedown event should have been fired.");
+ is(mouseUpEvents, 1,
+ "One mouseup event should have been fired.");
+ is(scrollEvents, 0,
+ "No scroll event should have been fired.");
+
+ info("Zooming in by scrolling inside the selection.");
+
+ scroll(graph, -1000, 400);
+ is(graph.getSelection().start, 375,
+ "The current selection start value is correct (2).");
+ is(graph.getSelection().end, 425,
+ "The current selection end value is correct (2).");
+
+ is(mouseDownEvents, 1,
+ "No more mousedown events should have been fired.");
+ is(mouseUpEvents, 1,
+ "No more mouseup events should have been fired.");
+ is(scrollEvents, 1,
+ "One scroll event should have been fired.");
+}
+
+// EventUtils just doesn't work!
+
+function dragStart(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseUp({ clientX: x, clientY: y });
+}
+
+function scroll(graph, wheel, x, y = 1) {
+ x /= window.devicePixelRatio;
+ y /= window.devicePixelRatio;
+ graph._onMouseMove({ clientX: x, clientY: y });
+ graph._onMouseWheel({ clientX: x, clientY: y, detail: wheel });
+}
diff --git a/toolkit/devtools/shared/test/browser_inplace-editor.js b/toolkit/devtools/shared/test/browser_inplace-editor.js
new file mode 100644
index 000000000..11718884b
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_inplace-editor.js
@@ -0,0 +1,123 @@
+/* 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";
+
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+let {editableField, getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
+
+// Test the inplace-editor behavior.
+
+add_task(function*() {
+ yield promiseTab("data:text/html;charset=utf-8,inline editor tests");
+ let [host, win, doc] = yield createHost();
+
+ yield testReturnCommit(doc);
+ yield testBlurCommit(doc);
+ yield testAdvanceCharCommit(doc);
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testReturnCommit(doc) {
+ info("Testing that pressing return commits the new value");
+ let def = promise.defer();
+
+ createInplaceEditorAndClick({
+ initial: "explicit initial",
+ start: function(editor) {
+ is(editor.input.value, "explicit initial", "Explicit initial value should be used.");
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("return");
+ },
+ done: onDone("Test Value", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testBlurCommit(doc) {
+ info("Testing that bluring the field commits the new value");
+ let def = promise.defer();
+
+ createInplaceEditorAndClick({
+ start: function(editor) {
+ is(editor.input.value, "Edit Me!", "textContent of the span used.");
+ editor.input.value = "Test Value";
+ editor.input.blur();
+ },
+ done: onDone("Test Value", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testAdvanceCharCommit(doc) {
+ info("Testing that configured advanceChars commit the new value");
+ let def = promise.defer();
+
+ createInplaceEditorAndClick({
+ advanceChars: ":",
+ start: function(editor) {
+ let input = editor.input;
+ for each (let ch in "Test:") {
+ EventUtils.sendChar(ch);
+ }
+ },
+ done: onDone("Test", true, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function testEscapeCancel(doc) {
+ info("Testing that escape cancels the new value");
+ let def = promise.defer();
+
+ createInplaceEditorAndClick({
+ initial: "initial text",
+ start: function(editor) {
+ editor.input.value = "Test Value";
+ EventUtils.sendKey("escape");
+ },
+ done: onDone("initial text", false, def)
+ }, doc);
+
+ return def.promise;
+}
+
+function onDone(value, isCommit, def) {
+ return function(actualValue, actualCommit) {
+ info("Inplace-editor's done callback executed, checking its state");
+ is(actualValue, value, "The value is correct");
+ is(actualCommit, isCommit, "The commit boolean is correct");
+ def.resolve();
+ }
+}
+
+function createInplaceEditorAndClick(options, doc) {
+ clearBody(doc);
+ let span = options.element = createSpan(doc);
+
+ info("Creating an inplace-editor field");
+ editableField(options);
+
+ info("Clicking on the inplace-editor field to turn to edit mode");
+ span.click();
+}
+
+function clearBody(doc) {
+ info("Clearing the page body");
+ doc.body.innerHTML = "";
+}
+
+function createSpan(doc) {
+ info("Creating a new span element");
+ let span = doc.createElement("span");
+ span.setAttribute("tabindex", "0");
+ span.textContent = "Edit Me!";
+ doc.body.appendChild(span);
+ return span;
+}
diff --git a/toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.html b/toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.html
new file mode 100644
index 000000000..070792b9a
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Layout Helpers</title>
+<style id="styles">
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ #hidden-node {
+ display: none;
+ }
+
+ #simple-node-with-margin-padding-border {
+ width: 200px;
+ height: 200px;
+ background: #f06;
+
+ padding: 20px;
+ margin: 50px;
+ border: 10px solid black;
+ }
+
+ #scrolled-node {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ width: 300px;
+ height: 100px;
+ overflow: scroll;
+ background: linear-gradient(red, pink);
+ }
+
+ #sub-scrolled-node {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ background: linear-gradient(yellow, green);
+ }
+
+ #inner-scrolled-node {
+ width: 100px;
+ height: 400px;
+ background: linear-gradient(black, white);
+ }
+</style>
+<div id="hidden-node"></div>
+<div id="simple-node-with-margin-padding-border"></div>
+<!-- The inline encoded code below corresponds to:
+<iframe style="margin:10px;border:0;width:300px;height:300px;">
+ <iframe style="margin:10px;border:0;width:200px;height:200px;">
+ <div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
+ </iframe>
+</iframe>
+ -->
+<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
+<div id="scrolled-node">
+ <div id="sub-scrolled-node">
+ <div id="inner-scrolled-node"></div>
+ </div>
+</div>
+<span id="inline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus porttitor luctus sem id scelerisque. Cras quis velit sed risus euismod lacinia. Donec viverra enim eu ligula efficitur, quis vulputate metus cursus. Duis sed interdum risus. Ut blandit velit vitae faucibus efficitur. Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br/ >
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed vitae dolor metus. Aliquam sed velit sit amet libero vestibulum aliquam vel a lorem. Integer eget ex eget justo auctor ullamcorper.<br/ >
+Praesent tristique maximus lacus, nec ultricies neque ultrices non. Phasellus vel lobortis justo. </span> \ No newline at end of file
diff --git a/toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js b/toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js
new file mode 100644
index 000000000..7fe1c84fc
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that LayoutHelpers.getAdjustedQuads works properly in a variety of use
+// cases including iframes, scroll and zoom
+
+const {utils: Cu} = Components;
+const {LayoutHelpers} = Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", {});
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ let doc = browser.contentDocument;
+ let win = doc.defaultView;
+
+ info("Creating a new LayoutHelpers instance for the test window");
+ let helper = new LayoutHelpers(win);
+ ok(helper.getAdjustedQuads, "getAdjustedQuads is defined");
+
+ info("Running tests");
+
+ returnsTheRightDataStructure(doc, helper);
+ isEmptyForMissingNode(doc, helper);
+ isEmptyForHiddenNodes(doc, helper);
+ defaultsToBorderBoxIfNoneProvided(doc, helper);
+ returnsLikeGetBoxQuadsInSimpleCase(doc, helper);
+ takesIframesOffsetsIntoAccount(doc, helper);
+ takesScrollingIntoAccount(doc, helper);
+ takesZoomIntoAccount(doc, helper);
+ returnsMultipleItemsForWrappingInlineElements(doc, helper);
+
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+
+function returnsTheRightDataStructure(doc, helper) {
+ info("Checks that the returned data contains bounds and 4 points");
+
+ let node = doc.querySelector("body");
+ let [res] = helper.getAdjustedQuads(node, "content");
+
+ ok("bounds" in res, "The returned data has a bounds property");
+ ok("p1" in res, "The returned data has a p1 property");
+ ok("p2" in res, "The returned data has a p2 property");
+ ok("p3" in res, "The returned data has a p3 property");
+ ok("p4" in res, "The returned data has a p4 property");
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ ok(pointProp in res[point], point + " has a " + pointProp + " property");
+ }
+ }
+}
+
+function isEmptyForMissingNode(doc, helper) {
+ info("Checks that null is returned for invalid nodes");
+
+ for (let input of [null, undefined, "", 0]) {
+ is(helper.getAdjustedQuads(input).length, 0, "A 0-length array is returned" +
+ "for input " + input);
+ }
+}
+
+function isEmptyForHiddenNodes(doc, helper) {
+ info("Checks that null is returned for nodes that aren't rendered");
+
+ let style = doc.querySelector("#styles");
+ is(helper.getAdjustedQuads(style).length, 0,
+ "null is returned for a <style> node");
+
+ let hidden = doc.querySelector("#hidden-node");
+ is(helper.getAdjustedQuads(hidden).length, 0,
+ "null is returned for a hidden node");
+}
+
+function defaultsToBorderBoxIfNoneProvided(doc, helper) {
+ info("Checks that if no boxtype is passed, then border is the default one");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [withBoxType] = helper.getAdjustedQuads(node, "border");
+ let [withoutBoxType] = helper.getAdjustedQuads(node);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
+ boundProp + " bound is equal with or without the border box type");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
+ point + "." + pointProp +
+ " is equal with or without the border box type");
+ }
+ }
+}
+
+function returnsLikeGetBoxQuadsInSimpleCase(doc, helper) {
+ info("Checks that for an element in the main frame, without scroll nor zoom" +
+ "that the returned value is similar to the returned value of getBoxQuads");
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+
+ for (let region of ["content", "padding", "border", "margin"]) {
+ let expected = node.getBoxQuads({
+ box: region
+ })[0];
+ let [actual] = helper.getAdjustedQuads(node, region);
+
+ for (let boundProp of
+ ["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
+ is(actual.bounds[boundProp], expected.bounds[boundProp],
+ boundProp + " bound is equal to the one returned by getBoxQuads for " +
+ region + " box");
+ }
+
+ for (let point of ["p1", "p2", "p3", "p4"]) {
+ for (let pointProp of ["x", "y", "z", "w"]) {
+ is(actual[point][pointProp], expected[point][pointProp],
+ point + "." + pointProp +
+ " is equal to the one returned by getBoxQuads for " + region + " box");
+ }
+ }
+ }
+}
+
+function takesIframesOffsetsIntoAccount(doc, helper) {
+ info("Checks that the quad returned for a node inside iframes that have " +
+ "margins takes those offsets into account");
+
+ let rootIframe = doc.querySelector("iframe");
+ let subIframe = rootIframe.contentDocument.querySelector("iframe");
+ let innerNode = subIframe.contentDocument.querySelector("#inner-node");
+
+ let [quad] = helper.getAdjustedQuads(innerNode, "content");
+
+ //rootIframe margin + subIframe margin + node margin + node border + node padding
+ let p1x = 10 + 10 + 10 + 10 + 10;
+ is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
+
+ // Same as p1x + the inner node width
+ let p2x = p1x + 100;
+ is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
+}
+
+function takesScrollingIntoAccount(doc, helper) {
+ info("Checks that the quad returned for a node inside multiple scrolled " +
+ "containers takes the scroll values into account");
+
+ // For info, the container being tested here is absolutely positioned at 0 0
+ // to simplify asserting the coordinates
+
+ info("Scroll the container nodes down");
+ let scrolledNode = doc.querySelector("#scrolled-node");
+ scrolledNode.scrollTop = 100;
+ let subScrolledNode = doc.querySelector("#sub-scrolled-node");
+ subScrolledNode.scrollTop = 200;
+ let innerNode = doc.querySelector("#inner-scrolled-node");
+
+ let [quad] = helper.getAdjustedQuads(innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
+ is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
+
+ info("Scrolling back up");
+ scrolledNode.scrollTop = 0;
+ subScrolledNode.scrollTop = 0;
+
+ [quad] = helper.getAdjustedQuads(innerNode, "content");
+ is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
+ is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
+}
+
+function takesZoomIntoAccount(doc, helper) {
+ info("Checks that if the page is zoomed in/out, the quad returned is correct");
+
+ // Hard-coding coordinates in this zoom test is a bad idea as it can vary
+ // depending on the platform, so we simply test that zooming in produces a
+ // bigger quad and zooming out produces a smaller quad
+
+ let node = doc.querySelector("#simple-node-with-margin-padding-border");
+ let [defaultQuad] = helper.getAdjustedQuads(node);
+
+ info("Zoom in");
+ window.FullZoom.enlarge();
+ let [zoomedInQuad] = helper.getAdjustedQuads(node);
+
+ ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
+ "The zoomed in quad is bigger than the default one");
+ ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
+ "The zoomed in quad is bigger than the default one");
+
+ info("Zoom out");
+ window.FullZoom.reset();
+ window.FullZoom.reduce();
+ let [zoomedOutQuad] = helper.getAdjustedQuads(node);
+
+ ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
+ "The zoomed out quad is smaller than the default one");
+ ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
+ "The zoomed out quad is smaller than the default one");
+
+ window.FullZoom.reset();
+}
+
+function returnsMultipleItemsForWrappingInlineElements(doc, helper) {
+ info("Checks that several quads are returned for inline elements that span line-breaks");
+
+ let node = doc.querySelector("#inline");
+ let quads = helper.getAdjustedQuads(node, "content");
+ // At least 3 because of the 2 <br />, maybe more depending on the window size.
+ ok(quads.length >= 3, "Multiple quads were returned");
+
+ is(quads.length, node.getBoxQuads().length,
+ "The same number of boxes as getBoxQuads was returned");
+}
diff --git a/toolkit/devtools/shared/test/browser_layoutHelpers.html b/toolkit/devtools/shared/test/browser_layoutHelpers.html
new file mode 100644
index 000000000..3b9a285b4
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_layoutHelpers.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+ iframe {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border: 0;
+ }
+</style>
+
+<div id=some></div>
+<iframe id=frame src='./browser_layoutHelpers_iframe.html'></iframe>
diff --git a/toolkit/devtools/shared/test/browser_layoutHelpers.js b/toolkit/devtools/shared/test/browser_layoutHelpers.js
new file mode 100644
index 000000000..3ee8665f1
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_layoutHelpers.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that scrollIntoViewIfNeeded works properly.
+
+let imported = {};
+Components.utils.import("resource://gre/modules/devtools/LayoutHelpers.jsm",
+ imported);
+registerCleanupFunction(function () {
+ imported = {};
+});
+
+let LayoutHelpers = imported.LayoutHelpers;
+
+const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_layoutHelpers.js");
+ let doc = browser.contentDocument;
+ runTest(doc.defaultView, doc.getElementById('some'));
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+
+function runTest(win, some) {
+ let lh = new LayoutHelpers(win);
+
+ some.style.top = win.innerHeight + 'px';
+ some.style.left = win.innerWidth + 'px';
+ // The tests start with a black 2x2 pixels square below bottom right.
+ // Do not resize the window during the tests.
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2); // Above the viewport.
+ lh.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ 'Element completely hidden above should appear centered.');
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1); // On the top edge.
+ lh.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, win.innerHeight,
+ 'Element partially visible above should appear above.');
+
+ win.scroll(win.innerWidth / 2, 0); // Just below the viewport.
+ lh.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
+ 'Element completely hidden below should appear centered.');
+
+ win.scroll(win.innerWidth / 2, 1); // On the bottom edge.
+ lh.scrollIntoViewIfNeeded(some);
+ is(win.scrollY, 2,
+ 'Element partially visible below should appear below.');
+
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 2); // Above the viewport.
+ lh.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ 'Element completely hidden above should appear above ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, win.innerHeight + 1); // On the top edge.
+ lh.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, win.innerHeight,
+ 'Element partially visible above should appear above ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, 0); // Below the viewport.
+ lh.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ 'Element completely hidden below should appear below ' +
+ 'if parameter is false.');
+
+ win.scroll(win.innerWidth / 2, 1); // On the bottom edge.
+ lh.scrollIntoViewIfNeeded(some, false);
+ is(win.scrollY, 2,
+ 'Element partially visible below should appear below ' +
+ 'if parameter is false.');
+
+ // The case of iframes.
+ win.scroll(0, 0);
+
+ let frame = win.document.getElementById('frame');
+ let fwin = frame.contentWindow;
+
+ frame.style.top = win.innerHeight + 'px';
+ frame.style.left = win.innerWidth + 'px';
+
+ fwin.addEventListener('load', function frameLoad() {
+ let some = fwin.document.getElementById('some');
+ lh.scrollIntoViewIfNeeded(some);
+ is(win.scrollX, Math.floor(win.innerWidth / 2) + 20,
+ 'Scrolling from an iframe should center the iframe vertically.');
+ is(win.scrollY, Math.floor(win.innerHeight / 2) + 20,
+ 'Scrolling from an iframe should center the iframe horizontally.');
+ is(fwin.scrollX, Math.floor(fwin.innerWidth / 2) + 1,
+ 'Scrolling from an iframe should center the element vertically.');
+ is(fwin.scrollY, Math.floor(fwin.innerHeight / 2) + 1,
+ 'Scrolling from an iframe should center the element horizontally.');
+ }, false);
+}
diff --git a/toolkit/devtools/shared/test/browser_layoutHelpers_iframe.html b/toolkit/devtools/shared/test/browser_layoutHelpers_iframe.html
new file mode 100644
index 000000000..66ef5b293
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_layoutHelpers_iframe.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset=utf-8>
+<title> Layout Helpers </title>
+
+<style>
+ html {
+ height: 300%;
+ width: 300%;
+ }
+ div#some {
+ position: absolute;
+ background: black;
+ width: 2px;
+ height: 2px;
+ }
+</style>
+
+<div id=some></div>
+
diff --git a/toolkit/devtools/shared/test/browser_num-l10n.js b/toolkit/devtools/shared/test/browser_num-l10n.js
new file mode 100644
index 000000000..a7a70abaa
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_num-l10n.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that ViewHelpers.Prefs work properly.
+
+let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+
+function test() {
+ let l10n = new ViewHelpers.L10N();
+
+ is(l10n.numberWithDecimals(1234.56789, 2), "1,234.56",
+ "The first number was properly localized.");
+ is(l10n.numberWithDecimals(0.0001, 2), "0",
+ "The second number was properly localized.");
+ is(l10n.numberWithDecimals(1.0001, 2), "1",
+ "The third number was properly localized.");
+ is(l10n.numberWithDecimals(NaN, 2), "0",
+ "NaN was properly localized.");
+ is(l10n.numberWithDecimals(null, 2), "0",
+ "`null` was properly localized.");
+ is(l10n.numberWithDecimals(undefined, 2), "0",
+ "`undefined` was properly localized.");
+
+ finish();
+}
diff --git a/toolkit/devtools/shared/test/browser_observableobject.js b/toolkit/devtools/shared/test/browser_observableobject.js
new file mode 100644
index 000000000..8bd1d5169
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_observableobject.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/devtools/Loader.jsm", tmp);
+ let ObservableObject = tmp.devtools.require("devtools/shared/observable-object");
+
+ let rawObject = {};
+ let oe = new ObservableObject(rawObject);
+
+ function str(o) {
+ return JSON.stringify(o);
+ }
+
+ function areObjectsSynced() {
+ is(str(rawObject), str(oe.object), "Objects are synced");
+ }
+
+ areObjectsSynced();
+
+ let index = 0;
+ let expected = [
+ {type: "set", path: "foo", value: 4},
+ {type: "get", path: "foo", value: 4},
+ {type: "get", path: "foo", value: 4},
+ {type: "get", path: "bar", value: undefined},
+ {type: "get", path: "bar", value: undefined},
+ {type: "set", path: "bar", value: {}},
+ {type: "get", path: "bar", value: {}},
+ {type: "get", path: "bar", value: {}},
+ {type: "set", path: "bar.a", value: [1,2,3,4]},
+ {type: "get", path: "bar", value: {a:[1,2,3,4]}},
+ {type: "set", path: "bar.mop", value: 1},
+ {type: "set", path: "bar", value: {}},
+ {type: "set", path: "foo", value: [{a:42}]},
+ {type: "get", path: "foo", value: [{a:42}]},
+ {type: "get", path: "foo.0", value: {a:42}},
+ {type: "get", path: "foo.0.a", value: 42},
+ {type: "get", path: "foo", value: [{a:42}]},
+ {type: "get", path: "foo.0", value: {a:42}},
+ {type: "set", path: "foo.0.a", value: 2},
+ {type: "get", path: "foo", value: [{a:2}]},
+ {type: "get", path: "bar", value: {}},
+ {type: "set", path: "foo.1", value: {}},
+ ];
+
+ function callback(event, path, value) {
+ oe.off("get", callback);
+ ok(event, "event defined");
+ ok(path, "path defined");
+ if (index >= expected.length) {
+ return;
+ }
+ let e = expected[index];
+ is(event, e.type, "[" + index + "] Right event received");
+ is(path.join("."), e.path, "[" + index + "] Path valid");
+ is(str(value), str(e.value), "[" + index + "] Value valid");
+ index++;
+ areObjectsSynced();
+ oe.on("get", callback);
+ }
+
+ oe.on("set", callback);
+ oe.on("get", callback);
+
+ oe.object.foo = 4;
+ oe.object.foo;
+ Object.getOwnPropertyDescriptor(oe.object, "foo")
+ oe.object["bar"];
+ oe.object.bar;
+ oe.object.bar = {};
+ oe.object.bar;
+ oe.object.bar.a = [1,2,3,4];
+ Object.defineProperty(oe.object.bar, "mop", {value:1});
+ oe.object.bar = {};
+ oe.object.foo = [{a:42}];
+ oe.object.foo[0].a;
+ oe.object.foo[0].a = 2;
+ oe.object.foo[1] = oe.object.bar;
+
+ is(index, expected.length, "Event count is right");
+ is(oe.object.bar, oe.object.bar, "Object attributes are wrapped only once");
+
+ finish();
+}
diff --git a/toolkit/devtools/shared/test/browser_options-view-01.js b/toolkit/devtools/shared/test/browser_options-view-01.js
new file mode 100644
index 000000000..398b5175a
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_options-view-01.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that options-view OptionsView responds to events correctly.
+
+const {OptionsView} = devtools.require("devtools/shared/options-view");
+const {Services} = devtools.require("resource://gre/modules/Services.jsm");
+
+const BRANCH = "devtools.debugger.";
+const BLACK_BOX_PREF = "auto-black-box";
+const PRETTY_PRINT_PREF = "auto-pretty-print";
+
+let originalBlackBox = Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF);
+let originalPrettyPrint = Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF);
+
+add_task(function*() {
+ info("Setting a couple of preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, true);
+
+ info("Opening a test tab and a toolbox host to create the options view in");
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", OPTIONS_VIEW_URL);
+
+ yield testOptionsView(win);
+
+ info("Closing the host and current tab");
+ host.destroy();
+ gBrowser.removeCurrentTab();
+
+ info("Resetting the preferences");
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
+});
+
+function* testOptionsView(win) {
+ let events = [];
+ let options = createOptionsView(win);
+ yield options.initialize();
+
+ let $ = win.document.querySelector.bind(win.document);
+
+ options.on("pref-changed", (_, pref) => events.push(pref));
+
+ let ppEl = $("menuitem[data-pref='auto-pretty-print']");
+ let bbEl = $("menuitem[data-pref='auto-black-box']");
+
+ // Test default config
+ is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
+ is(bbEl.getAttribute("checked"), "", "`false` prefs are unchecked on start");
+
+ // Test buttons update when preferences update outside of the menu
+ Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, false);
+ Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, true);
+
+ is(options.getPref(PRETTY_PRINT_PREF), false, "getPref returns correct value");
+ is(options.getPref(BLACK_BOX_PREF), true, "getPref returns correct value");
+
+ is(ppEl.getAttribute("checked"), "", "menuitems update when preferences change");
+ is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
+
+ // Tests events are fired when preferences update outside of the menu
+ is(events.length, 2, "two 'pref-changed' events fired");
+ is(events[0], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[1], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ // Test buttons update when clicked and preferences are updated
+ yield click(options, win, ppEl);
+ is(ppEl.getAttribute("checked"), "true", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF), true, "preference updated via click");
+
+ yield click(options, win, bbEl);
+ is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
+ is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF), false, "preference updated via click");
+
+ // Tests events are fired when preferences updated via click
+ is(events.length, 4, "two 'pref-changed' events fired");
+ is(events[2], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
+ is(events[3], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
+
+ yield options.destroy();
+}
+
+function createOptionsView(win) {
+ return new OptionsView({
+ branchName: BRANCH,
+ menupopup: win.document.querySelector("#options-menupopup")
+ });
+}
+
+function* click(view, win, menuitem) {
+ let opened = view.once("options-shown");
+ let closed = view.once("options-hidden");
+
+ let button = win.document.querySelector("#options-button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, win);
+ yield opened;
+
+ EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
+ yield closed;
+}
diff --git a/toolkit/devtools/shared/test/browser_outputparser.js b/toolkit/devtools/shared/test/browser_outputparser.js
new file mode 100644
index 000000000..583f74fad
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_outputparser.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+let {OutputParser} = devtools.require("devtools/output-parser");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost("bottom", "data:text/html," +
+ "<h1>browser_outputParser.js</h1><div></div>");
+
+ let parser = new OutputParser();
+ testParseCssProperty(doc, parser);
+ testParseCssVar(doc, parser);
+ testParseHTMLAttribute(doc, parser);
+ testParseNonCssHTMLAttribute(doc, parser);
+
+ host.destroy();
+}
+
+function testParseCssProperty(doc, parser) {
+ let frag = parser.parseCssProperty("border", "1px solid red", {
+ colorSwatchClass: "test-colorswatch"
+ });
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ is(target.innerHTML,
+ '1px solid <span data-color="#F00"><span style="background-color:red" class="test-colorswatch"></span><span>#F00</span></span>',
+ "CSS property correctly parsed");
+
+ target.innerHTML = "";
+
+ frag = parser.parseCssProperty("background-image", "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))", {
+ colorSwatchClass: "test-colorswatch",
+ colorClass: "test-color"
+ });
+ target.appendChild(frag);
+ is(target.innerHTML,
+ 'linear-gradient(to right, <span data-color="#F60"><span style="background-color:#F60" class="test-colorswatch"></span><span class="test-color">#F60</span></span> 10%, ' +
+ '<span data-color="#000"><span style="background-color:rgba(0,0,0,1)" class="test-colorswatch"></span><span class="test-color">#000</span></span>)',
+ "Gradient CSS property correctly parsed");
+
+ target.innerHTML = "";
+}
+
+function testParseCssVar(doc, parser) {
+ let frag = parser.parseCssProperty("color", "var(--some-kind-of-green)", {
+ colorSwatchClass: "test-colorswatch"
+ });
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ is(target.innerHTML, "var(--some-kind-of-green)", "CSS property correctly parsed");
+
+ target.innerHTML = "";
+}
+
+function testParseHTMLAttribute(doc, parser) {
+ let attrib = "color:red; font-size: 12px; background-image: " +
+ "url(chrome://branding/content/about-logo.png)";
+ let frag = parser.parseHTMLAttribute(attrib, {
+ urlClass: "theme-link",
+ colorClass: "theme-color"
+ });
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ let expected = 'color:<span data-color="#F00"><span class="theme-color">#F00</span></span>; font-size: 12px; ' +
+ 'background-image: url("<a href="chrome://branding/content/about-logo.png" ' +
+ 'class="theme-link" ' +
+ 'target="_blank">chrome://branding/content/about-logo.png</a>")';
+
+ is(target.innerHTML, expected, "HTML Attribute correctly parsed");
+ target.innerHTML = "";
+}
+
+function testParseNonCssHTMLAttribute(doc, parser) {
+ let attrib = "someclass background someotherclass red";
+ let frag = parser.parseHTMLAttribute(attrib);
+
+ let target = doc.querySelector("div");
+ ok(target, "captain, we have the div");
+ target.appendChild(frag);
+
+ let expected = 'someclass background someotherclass red';
+
+ is(target.innerHTML, expected, "Non-CSS HTML Attribute correctly parsed");
+ target.innerHTML = "";
+}
diff --git a/toolkit/devtools/shared/test/browser_prefs.js b/toolkit/devtools/shared/test/browser_prefs.js
new file mode 100644
index 000000000..3d6b99c0a
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_prefs.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that ViewHelpers.Prefs work properly.
+
+let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+
+function test() {
+ let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
+ "foo": ["Bool", "enabled"]
+ });
+
+ let originalPrefValue = Services.prefs.getBoolPref("devtools.debugger.enabled");
+ is(Prefs.foo, originalPrefValue, "The pref value was correctly fetched.");
+
+ Prefs.foo = !originalPrefValue;
+ is(Prefs.foo, !originalPrefValue,
+ "The pref was was correctly changed (1).");
+ is(Services.prefs.getBoolPref("devtools.debugger.enabled"), !originalPrefValue,
+ "The pref was was correctly changed (2).");
+
+ Services.prefs.setBoolPref("devtools.debugger.enabled", originalPrefValue);
+ info("The pref value was reset.");
+
+ is(Prefs.foo, !originalPrefValue,
+ "The cached pref value hasn't changed yet.");
+
+ Prefs.refresh();
+ is(Prefs.foo, originalPrefValue,
+ "The cached pref value has changed now.");
+
+ finish();
+}
diff --git a/toolkit/devtools/shared/test/browser_require_basic.js b/toolkit/devtools/shared/test/browser_require_basic.js
new file mode 100644
index 000000000..f86974df4
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_require_basic.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that source URLs in the Web Console can be clicked to display the
+// standard View Source window.
+
+let [ define, require ] = (function() {
+ let tempScope = {};
+ Components.utils.import("resource://gre/modules/devtools/Require.jsm", tempScope);
+ return [ tempScope.define, tempScope.require ];
+})();
+
+function test() {
+ addTab("about:blank", function() {
+ info("Starting Require Tests");
+ setup();
+
+ testWorking();
+ testDomains();
+ testLeakage();
+ testMultiImport();
+ testRecursive();
+ testUncompilable();
+ testFirebug();
+
+ shutdown();
+ });
+}
+
+function setup() {
+ define('gclitest/requirable', [ 'require', 'exports', 'module' ], function(require, exports, module) {
+ exports.thing1 = 'thing1';
+ exports.thing2 = 2;
+
+ let status = 'initial';
+ exports.setStatus = function(aStatus) { status = aStatus; };
+ exports.getStatus = function() { return status; };
+ });
+
+ define('gclitest/unrequirable', [ 'require', 'exports', 'module' ], function(require, exports, module) {
+ null.throwNPE();
+ });
+
+ define('gclitest/recurse', [ 'require', 'exports', 'module', 'gclitest/recurse' ], function(require, exports, module) {
+ require('gclitest/recurse');
+ });
+
+ define('gclitest/firebug', [ 'gclitest/requirable' ], function(requirable) {
+ return { requirable: requirable, fb: true };
+ });
+}
+
+function shutdown() {
+ delete define.modules['gclitest/requirable'];
+ delete define.globalDomain.modules['gclitest/requirable'];
+ delete define.modules['gclitest/unrequirable'];
+ delete define.globalDomain.modules['gclitest/unrequirable'];
+ delete define.modules['gclitest/recurse'];
+ delete define.globalDomain.modules['gclitest/recurse'];
+ delete define.modules['gclitest/firebug'];
+ delete define.globalDomain.modules['gclitest/firebug'];
+
+ define = undefined;
+ require = undefined;
+
+ finish();
+}
+
+function testWorking() {
+ // There are lots of requirement tests that we could be doing here
+ // The fact that we can get anything at all working is a testament to
+ // require doing what it should - we don't need to test the
+ let requireable = require('gclitest/requirable');
+ is('thing1', requireable.thing1, 'thing1 was required');
+ is(2, requireable.thing2, 'thing2 was required');
+ is(requireable.thing3, undefined, 'thing3 was not required');
+}
+
+function testDomains() {
+ let requireable = require('gclitest/requirable');
+ is(requireable.status, undefined, 'requirable has no status');
+ requireable.setStatus(null);
+ is(null, requireable.getStatus(), 'requirable.getStatus changed to null');
+ is(requireable.status, undefined, 'requirable still has no status');
+ requireable.setStatus('42');
+ is('42', requireable.getStatus(), 'requirable.getStatus changed to 42');
+ is(requireable.status, undefined, 'requirable *still* has no status');
+
+ let domain = new define.Domain();
+ let requireable2 = domain.require('gclitest/requirable');
+ is(requireable2.status, undefined, 'requirable2 has no status');
+ is('initial', requireable2.getStatus(), 'requirable2.getStatus is initial');
+ requireable2.setStatus(999);
+ is(999, requireable2.getStatus(), 'requirable2.getStatus changed to 999');
+ is(requireable2.status, undefined, 'requirable2 still has no status');
+
+ is('42', requireable.getStatus(), 'status 42');
+ ok(requireable.status === undefined, 'requirable has no status (as expected)');
+
+ delete domain.modules['gclitest/requirable'];
+}
+
+function testLeakage() {
+ let requireable = require('gclitest/requirable');
+ is(requireable.setup, null, 'leakage of setup');
+ is(requireable.shutdown, null, 'leakage of shutdown');
+ is(requireable.testWorking, null, 'leakage of testWorking');
+}
+
+function testMultiImport() {
+ let r1 = require('gclitest/requirable');
+ let r2 = require('gclitest/requirable');
+ is(r1, r2, 'double require was strict equal');
+}
+
+function testUncompilable() {
+ // It's not totally clear how a module loader should perform with unusable
+ // modules, however at least it should go into a flat spin ...
+ // GCLI mini_require reports an error as it should
+ try {
+ let unrequireable = require('gclitest/unrequirable');
+ fail();
+ }
+ catch (ex) {
+ // an exception is expected
+ }
+}
+
+function testRecursive() {
+ // See Bug 658583
+ // require('gclitest/recurse');
+ // Also see the comments in the testRecursive() function
+}
+
+function testFirebug() {
+ let requirable = require('gclitest/requirable');
+ let firebug = require('gclitest/firebug');
+ ok(firebug.fb, 'firebug.fb is true');
+ is(requirable, firebug.requirable, 'requirable pass-through');
+}
diff --git a/toolkit/devtools/shared/test/browser_spectrum.js b/toolkit/devtools/shared/test/browser_spectrum.js
new file mode 100644
index 000000000..0812a8caf
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_spectrum.js
@@ -0,0 +1,114 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the spectrum color picker works correctly
+
+const TEST_URI = "chrome://browser/content/devtools/spectrum-frame.xhtml";
+const {Spectrum} = devtools.require("devtools/shared/widgets/Spectrum");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ yield performTest();
+ gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ yield testCreateAndDestroyShouldAppendAndRemoveElements(doc);
+ yield testPassingAColorAtInitShouldSetThatColor(doc);
+ yield testSettingAndGettingANewColor(doc);
+ yield testChangingColorShouldEmitEvents(doc);
+ yield testSettingColorShoudUpdateTheUI(doc);
+
+ host.destroy();
+}
+
+function testCreateAndDestroyShouldAppendAndRemoveElements(doc) {
+ let containerElement = doc.querySelector("#spectrum");
+ ok(containerElement, "We have the root node to append spectrum to");
+ is(containerElement.childElementCount, 0, "Root node is empty");
+
+ let s = new Spectrum(containerElement, [255, 126, 255, 1]);
+ s.show();
+ ok(containerElement.childElementCount > 0, "Spectrum has appended elements");
+
+ s.destroy();
+ is(containerElement.childElementCount, 0, "Destroying spectrum removed all nodes");
+}
+
+function testPassingAColorAtInitShouldSetThatColor(doc) {
+ let initRgba = [255, 126, 255, 1];
+
+ let s = new Spectrum(doc.querySelector("#spectrum"), initRgba);
+ s.show();
+
+ let setRgba = s.rgb;
+
+ is(initRgba[0], setRgba[0], "Spectrum initialized with the right color");
+ is(initRgba[1], setRgba[1], "Spectrum initialized with the right color");
+ is(initRgba[2], setRgba[2], "Spectrum initialized with the right color");
+ is(initRgba[3], setRgba[3], "Spectrum initialized with the right color");
+
+ s.destroy();
+}
+
+function testSettingAndGettingANewColor(doc) {
+ let s = new Spectrum(doc.querySelector("#spectrum"), [0, 0, 0, 1]);
+ s.show();
+
+ let colorToSet = [255, 255, 255, 1];
+ s.rgb = colorToSet;
+ let newColor = s.rgb;
+
+ is(colorToSet[0], newColor[0], "Spectrum set with the right color");
+ is(colorToSet[1], newColor[1], "Spectrum set with the right color");
+ is(colorToSet[2], newColor[2], "Spectrum set with the right color");
+ is(colorToSet[3], newColor[3], "Spectrum set with the right color");
+
+ s.destroy();
+}
+
+function testChangingColorShouldEmitEvents(doc) {
+ return new Promise(resolve => {
+ let s = new Spectrum(doc.querySelector("#spectrum"), [255, 255, 255, 1]);
+ s.show();
+
+ s.once("changed", (event, rgba, color) => {
+ ok(true, "Changed event was emitted on color change");
+ is(rgba[0], 128, "New color is correct");
+ is(rgba[1], 64, "New color is correct");
+ is(rgba[2], 64, "New color is correct");
+ is(rgba[3], 1, "New color is correct");
+ is("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " + rgba[3] + ")", color, "RGBA and css color correspond");
+
+ s.destroy();
+ resolve();
+ });
+
+ // Simulate a drag move event by calling the handler directly.
+ s.onDraggerMove(s.dragger.offsetWidth/2, s.dragger.offsetHeight/2);
+ });
+}
+
+function testSettingColorShoudUpdateTheUI(doc) {
+ let s = new Spectrum(doc.querySelector("#spectrum"), [255, 255, 255, 1]);
+ s.show();
+ let dragHelperOriginalPos = [s.dragHelper.style.top, s.dragHelper.style.left];
+ let alphaHelperOriginalPos = s.alphaSliderHelper.style.left;
+
+ s.rgb = [50, 240, 234, .2];
+ s.updateUI();
+
+ ok(s.alphaSliderHelper.style.left != alphaHelperOriginalPos, "Alpha helper has moved");
+ ok(s.dragHelper.style.top !== dragHelperOriginalPos[0], "Drag helper has moved");
+ ok(s.dragHelper.style.left !== dragHelperOriginalPos[1], "Drag helper has moved");
+
+ s.rgb = [240, 32, 124, 0];
+ s.updateUI();
+ is(s.alphaSliderHelper.style.left, - (s.alphaSliderHelper.offsetWidth/2) + "px",
+ "Alpha range UI has been updated again");
+
+ s.destroy();
+}
diff --git a/toolkit/devtools/shared/test/browser_tableWidget_basic.js b/toolkit/devtools/shared/test/browser_tableWidget_basic.js
new file mode 100644
index 000000000..ba0ac4c83
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_tableWidget_basic.js
@@ -0,0 +1,382 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the table widget api works fine
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/common.css'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/widgets.css'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'><box flex='1'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = devtools.require("devtools/shared/widgets/TableWidget");
+
+let doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ firstColumn: "col4"
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+function startTests() {
+ populateTable();
+ testTreeItemInsertedCorrectly();
+ testAPI();
+ endTests();
+}
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+
+ let span = doc.createElement("span");
+ span.textContent = "domnode";
+
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: span
+ });
+}
+
+/**
+ * Test if the nodes are inserted correctly in the table.
+ */
+function testTreeItemInsertedCorrectly() {
+ is(table.tbody.children.length, 4*2 /* double because splitters */,
+ "4 columns exist");
+
+ // Test firstColumn option and check if the nodes are inserted correctly
+ is(table.tbody.children[0].firstChild.children.length, 9 + 1 /* header */,
+ "Correct rows in column 4");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Column 4",
+ "Correct column header value");
+
+ for (let i = 1; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1 /* header */,
+ "Correct rows in column " + i);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value, "Column " + i,
+ "Correct column header value");
+ }
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.children[2].firstChild.children[i].value, "id" + i,
+ "Correct value in row " + i);
+ }
+
+ // Remove firstColumn option and reset the table
+ table.clear();
+ table.firstColumn = "";
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ populateTable();
+
+ // Check if the nodes are inserted correctly without firstColumn option
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i * 2].firstChild.children.length, 9 + 1 /* header */,
+ "Correct rows in column " + i);
+ is(table.tbody.children[i * 2].firstChild.firstChild.value, "Column " + (i + 1),
+ "Correct column header value");
+ }
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, "id" + i,
+ "Correct value in row " + i);
+ }
+}
+
+/**
+ * Tests if the API exposed by TableWidget works properly
+ */
+function testAPI() {
+ info("Testing TableWidget API");
+ // Check if selectRow and selectedRow setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ table.selectRow("id4");
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Somthing got selected");
+ is(node.getAttribute("data-id"), "id4", "Correct node selected");
+
+ table.selectRow("id7");
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Somthing is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.getAttribute("data-id"), "id7", "Correct node selected");
+
+ // test if selectedIRow getter works
+ is(table.selectedRow["col1"], "id7", "Correct result of selectedRow getter");
+
+ // test if isSelected works
+ ok(table.isSelected("id7"), "isSelected with column id works");
+ ok(table.isSelected({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ }), "isSelected with json works");
+
+ table.selectedRow = "id4";
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Somthing is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.getAttribute("data-id"), "id4", "Correct node selected");
+
+ // test if selectedRow getter works
+ is(table.selectedRow["col1"], "id4", "Correct result of selectedRow getter");
+
+ // test if clear selection works
+ table.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call");
+
+ // test if selectNextRow and selectPreviousRow work
+ table.selectedRow = "id7";
+ ok(table.isSelected("id7"), "Correct row selected");
+ table.selectNextRow();
+ ok(table.isSelected("id8"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id9"), "Correct row selected after selectNextRow call");
+
+ table.selectNextRow();
+ ok(table.isSelected("id1"),
+ "Properly cycled to first row after selectNextRow call on last row");
+
+ table.selectNextRow();
+ ok(table.isSelected("id2"), "Correct row selected after selectNextRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id1"), "Correct row selected after selectPreviousRow call");
+
+ table.selectPreviousRow();
+ ok(table.isSelected("id9"),
+ "Properly cycled to last row after selectPreviousRow call on first row");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='id4']"), "id4 row exists before removal");
+ table.remove("id4");
+ ok(!doc.querySelector("[data-id='id4']"),
+ "id4 row does not exist after removal through id");
+
+ ok(doc.querySelector("[data-id='id6']"), "id6 row exists before removal");
+ table.remove({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ ok(!doc.querySelector("[data-id='id6']"),
+ "id6 row does not exist after removal through json");
+
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+
+ // test if selectedIndex getter setter works
+ table.selectedIndex = 2;
+ ok(table.isSelected("id3"), "Correct row selected by selectedIndex setter");
+
+ table.selectedIndex = 4;
+ ok(table.isSelected("id5"), "Correct row selected by selectedIndex setter");
+
+ table.selectRow("id8");
+ is(table.selectedIndex, 7, "Correct value of selectedIndex getter");
+
+ // testing if clear works
+ table.clear();
+ is(table.tbody.children.length, 4*2 /* double because splitters */,
+ "4 columns exist even after clear");
+ for (let i = 0; i < 4; i++) {
+ is(table.tbody.children[i*2].firstChild.children.length, 1 /* header */,
+ "Only header in the column " + i + " after clear call");
+ is(table.tbody.children[i*2].firstChild.firstChild.value, "Column " + (i + 1),
+ "Correct column header value");
+ }
+
+ // testing if setColumns work
+ table.setColumns({
+ col1: "Foobar",
+ col2: "Testing"
+ });
+
+ is(table.tbody.children.length, 2*2 /* double because splitters */,
+ "2 columns exist after setColumn call");
+ is(table.tbody.children[0].firstChild.firstChild.value, "Foobar",
+ "Correct column header value for first column");
+ is(table.tbody.children[2].firstChild.firstChild.value, "Testing",
+ "Correct column header value for second column");
+
+ table.setColumns({
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ });
+ is(table.tbody.children.length, 4*2 /* double because splitters */,
+ "4 columns exist after second setColumn call");
+
+ populateTable();
+
+ // testing if update works
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "value12",
+ "Correct value before update");
+ table.update({
+ col1: "id4",
+ col2: "UPDATED",
+ col3: "value26",
+ col4: "value33"
+ });
+ is(doc.querySelectorAll("[data-id='id4']")[1].value, "UPDATED",
+ "Correct value after update");
+
+ // testing if sorting works
+ // calling it once on an already sorted column should sort in descending manner
+ table.sortBy("col1");
+ for (let i = 1; i < 10; i++) {
+ is(table.tbody.firstChild.firstChild.children[i].value, "id" + (10 - i),
+ "Correct value in row " + i + " after descending sort by on col1");
+ }
+ // Calling it on an unsorted column should sort by it in ascending manner
+ table.sortBy("col2");
+ let cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ // Calling it again should sort by it in descending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.lastChild.previousSibling;
+ checkDescendingOrder(cell);
+
+ // Calling it again should sort by it in ascending manner
+ table.sortBy("col2");
+ cell = table.tbody.children[2].firstChild.children[2];
+ checkAscendingOrder(cell);
+
+ table.clear();
+ populateTable();
+
+ // testing if sorting works should sort by ascending manner
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[1];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkAscendingOrder(cell.nextSibling);
+
+ // Calling it again should sort it in descending order
+ table.sortBy("col4");
+ cell = table.tbody.children[6].firstChild.children[9];
+ is(cell.textContent, "domnode", "DOMNode sorted correctly");
+ checkDescendingOrder(cell.previousSibling);
+}
+
+function checkAscendingOrder(cell) {
+ while(cell) {
+ let currentCell = cell.value || cell.textContent;
+ let prevCell = cell.previousSibling.value || cell.previousSibling.textContent;
+ ok(currentCell >= prevCell, "Sorting is in ascending order");
+ cell = cell.nextSibling;
+ }
+}
+
+function checkDescendingOrder(cell) {
+ while(cell != cell.parentNode.firstChild) {
+ let currentCell = cell.value || cell.textContent;
+ let nextCell = cell.nextSibling.value || cell.nextSibling.textContent;
+ ok(currentCell >= nextCell, "Sorting is in descending order");
+ cell = cell.previousSibling;
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_tableWidget_keyboard_interaction.js b/toolkit/devtools/shared/test/browser_tableWidget_keyboard_interaction.js
new file mode 100644
index 000000000..0ec5355e0
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_tableWidget_keyboard_interaction.js
@@ -0,0 +1,227 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that keyboard interaction works fine with the table widget
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/common.css'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/widgets.css'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'><box flex='1'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = devtools.require("devtools/shared/widgets/TableWidget");
+let {Task} = devtools.require("resource://gre/modules/Task.jsm");
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+let doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+let startTests = Task.async(function*() {
+ populateTable();
+ yield testKeyboardInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+/**
+ * Tests if pressing navigation keys on the table items does the expected behavior
+ */
+let testKeyboardInteraction = Task.async(function*() {
+ info("Testing keyboard interaction with the table");
+ info("clicking on first row");
+ let node = table.tbody.firstChild.firstChild.children[1];
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ click(node);
+ let [name, id] = yield event;
+
+ node = table.tbody.firstChild.firstChild.children[2];
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info("Pressing down key to select next row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey("DOWN", doc.defaultView);
+ id = yield event;
+ is(id, "id2", "Correct row was selected after pressing down");
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+ let nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), "id2",
+ "Correct cell selected in all columns");
+ }
+
+ node = table.tbody.firstChild.firstChild.children[3];
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info("Pressing down key to select next row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey("DOWN", doc.defaultView);
+ id = yield event;
+ is(id, "id3", "Correct row was selected after pressing down");
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+ nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), "id3",
+ "Correct cell selected in all columns");
+ }
+
+ // pressing up arrow key to select previous row
+ node = table.tbody.firstChild.firstChild.children[2];
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info("Pressing up key to select previous row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey("UP", doc.defaultView);
+ id = yield event;
+ is(id, "id2", "Correct row was selected after pressing down");
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+ nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), "id2",
+ "Correct cell selected in all columns");
+ }
+
+ // selecting last item node to test edge navigation cycling case
+ table.selectedRow = "id9";
+ // pressing down now should move to first row.
+ node = table.tbody.firstChild.firstChild.children[1];
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info("Pressing down key on last row to select first row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey("DOWN", doc.defaultView);
+ id = yield event;
+ is(id, "id1", "Correct row was selected after pressing down");
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+ nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), "id1",
+ "Correct cell selected in all columns");
+ }
+
+ // pressing up now should move to last row.
+ node = table.tbody.firstChild.firstChild.lastChild;
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"),
+ "Row should not have selected class");
+ info("Pressing down key on last row to select first row");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ EventUtils.sendKey("UP", doc.defaultView);
+ id = yield event;
+ is(id, "id9", "Correct row was selected after pressing down");
+ ok(node.classList.contains("theme-selected"), "row has selected class");
+ nodes = doc.querySelectorAll(".theme-selected");
+ for (let i = 0; i < nodes.length; i++) {
+ is(nodes[i].getAttribute("data-id"), "id9",
+ "Correct cell selected in all columns");
+ }
+});
diff --git a/toolkit/devtools/shared/test/browser_tableWidget_mouse_interaction.js b/toolkit/devtools/shared/test/browser_tableWidget_mouse_interaction.js
new file mode 100644
index 000000000..efdc8ee07
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_tableWidget_mouse_interaction.js
@@ -0,0 +1,298 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that mosue interaction works fine with the table widget
+
+const TEST_URI = "data:text/xml;charset=UTF-8,<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/common.css'?>" +
+ "<?xml-stylesheet href='chrome://browser/skin/devtools/widgets.css'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='Table Widget' width='600' height='500'><box flex='1'/></window>";
+const TEST_OPT = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+const {TableWidget} = devtools.require("devtools/shared/widgets/TableWidget");
+let {Task} = devtools.require("resource://gre/modules/Task.jsm");
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+let doc, table;
+
+function test() {
+ waitForExplicitFinish();
+ let win = Services.ww.openWindow(null, TEST_URI, "_blank", TEST_OPT, null);
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ doc = win.document;
+ table = new TableWidget(doc.querySelector("box"), {
+ initialColumns: {
+ col1: "Column 1",
+ col2: "Column 2",
+ col3: "Column 3",
+ col4: "Column 4"
+ },
+ uniqueId: "col1",
+ emptyText: "This is dummy empty text",
+ highlightUpdated: true,
+ removableColumns: true,
+ });
+ startTests();
+ });
+ });
+}
+
+function endTests() {
+ table.destroy();
+ doc.defaultView.close();
+ doc = table = null;
+ finish();
+}
+
+let startTests = Task.async(function*() {
+ populateTable();
+ yield testMouseInteraction();
+ endTests();
+});
+
+function populateTable() {
+ table.push({
+ col1: "id1",
+ col2: "value10",
+ col3: "value20",
+ col4: "value30"
+ });
+ table.push({
+ col1: "id2",
+ col2: "value14",
+ col3: "value29",
+ col4: "value32"
+ });
+ table.push({
+ col1: "id3",
+ col2: "value17",
+ col3: "value21",
+ col4: "value31",
+ extraData: "foobar",
+ extraData2: 42
+ });
+ table.push({
+ col1: "id4",
+ col2: "value12",
+ col3: "value26",
+ col4: "value33"
+ });
+ table.push({
+ col1: "id5",
+ col2: "value19",
+ col3: "value26",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id6",
+ col2: "value15",
+ col3: "value25",
+ col4: "value37"
+ });
+ table.push({
+ col1: "id7",
+ col2: "value18",
+ col3: "value21",
+ col4: "value36",
+ somethingExtra: "Hello World!"
+ });
+ table.push({
+ col1: "id8",
+ col2: "value11",
+ col3: "value27",
+ col4: "value34"
+ });
+ table.push({
+ col1: "id9",
+ col2: "value11",
+ col3: "value23",
+ col4: "value38"
+ });
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node, button = 0) {
+ if (button == 0) {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, doc.defaultView));
+ } else {
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {
+ button: button,
+ type: "contextmenu"
+ }, doc.defaultView));
+ }
+}
+
+/**
+ * Tests if clicking the table items does the expected behavior
+ */
+let testMouseInteraction = Task.async(function*() {
+ info("Testing mouse interaction with the table");
+ ok(!table.selectedRow, "Nothing should be selected beforehand");
+
+ let event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let node = table.tbody.firstChild.firstChild.children[1];
+ info("clicking on the first row");
+ ok(!node.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(node);
+ let id = yield event;
+ ok(node.classList.contains("theme-selected"), "Node has selected class after click");
+ is(id, "id1", "Correct row was selected");
+
+ info("clicking on third row to select it");
+ event = table.once(TableWidget.EVENTS.ROW_SELECTED);
+ let node2 = table.tbody.firstChild.firstChild.children[3];
+ // node should not have selected class
+ ok(!node2.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ click(node2);
+ id = yield event;
+ ok(node2.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(id, "id3", "Correct table path is emitted for new node")
+ isnot(node, node2, "Old and new node are different");
+ ok(!node.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+ // clicking on table header to sort by it
+ event = table.once(TableWidget.EVENTS.COLUMN_SORTED);
+ node = table.tbody.children[6].firstChild.children[0];
+ info("clicking on the 4th coulmn header to sort the table by it");
+ ok(!node.hasAttribute("sorted"),
+ "Node should not have sorted attribute before clicking");
+ ok(doc.querySelector("[sorted]"), "Although, something else should be sorted on");
+ isnot(doc.querySelector("[sorted]"), node, "Which is not equal to this node");
+ click(node);
+ id = yield event;
+ is(id, "col4", "Correct column was sorted on");
+ ok(node.hasAttribute("sorted"),
+ "Node should now have sorted attribute after clicking");
+ is(doc.querySelectorAll("[sorted]").length, 1,
+ "Now only one column should be sorted on");
+ is(doc.querySelector("[sorted]"), node, "Which should be this column");
+
+ // test context menu opening.
+ // hiding second column
+ // event listener for popupshown
+ event = Promise.defer();
+ table.menupopup.addEventListener("popupshown", function onPopupShown(e) {
+ table.menupopup.removeEventListener("popupshown", onPopupShown);
+ event.resolve();
+ })
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ click(node, 2);
+ yield event.promise;
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ is(table.menupopup.querySelector("[disabled]"),
+ table.menupopup.querySelector("[data-id='col1']"),
+ "Which is the unique column");
+ // popup should be open now
+ // clicking on second column label
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // hiding third column
+ // event listener for popupshown
+ event = Promise.defer();
+ table.menupopup.addEventListener("popupshown", function onPopupShown(e) {
+ table.menupopup.removeEventListener("popupshown", onPopupShown);
+ event.resolve();
+ })
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ click(node, 2);
+ yield event.promise;
+ is(table.menupopup.querySelectorAll("[disabled]").length, 1,
+ "Only 1 menuitem is disabled");
+ // popup should be open now
+ // clicking on second column label
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden before hiding it");
+ click(node);
+ id = yield event;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden after hiding it");
+
+ // opening again to see if 2 items are disabled now
+ // event listener for popupshown
+ event = Promise.defer();
+ table.menupopup.addEventListener("popupshown", function onPopupShown(e) {
+ table.menupopup.removeEventListener("popupshown", onPopupShown);
+ event.resolve();
+ })
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ click(node, 2);
+ yield event.promise;
+ is(table.menupopup.querySelectorAll("[disabled]").length, 2,
+ "2 menuitems are disabled now as only 2 columns remain visible");
+ is(table.menupopup.querySelectorAll("[disabled]")[0],
+ table.menupopup.querySelector("[data-id='col1']"),
+ "First is the unique column");
+ is(table.menupopup.querySelectorAll("[disabled]")[1],
+ table.menupopup.querySelector("[data-id='col4']"),
+ "Second is the last column");
+
+ // showing back 2nd column
+ // popup should be open now
+ // clicking on second column label
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col2']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[2].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ is(id, "col2", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[2].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // showing back 3rd column
+ // event listener for popupshown
+ event = Promise.defer();
+ table.menupopup.addEventListener("popupshown", function onPopupShown(e) {
+ table.menupopup.removeEventListener("popupshown", onPopupShown);
+ event.resolve();
+ })
+ info("right clicking on the first column header");
+ node = table.tbody.firstChild.firstChild.firstChild;
+ click(node, 2);
+ yield event.promise;
+ // popup should be open now
+ // clicking on second column label
+ event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
+ node = table.menupopup.querySelector("[data-id='col3']");
+ info("selecting to hide the second column");
+ is(table.tbody.children[4].getAttribute("hidden"), "true",
+ "Column is hidden before unhiding it");
+ click(node);
+ id = yield event;
+ is(id, "col3", "Correct column was triggered to be hidden");
+ ok(!table.tbody.children[4].hasAttribute("hidden"),
+ "Column is not hidden after unhiding it");
+
+ // reset table state
+ table.clearSelection();
+ table.sortBy("col1");
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_button_eyedropper.js b/toolkit/devtools/shared/test/browser_telemetry_button_eyedropper.js
new file mode 100644
index 000000000..946e4174e
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_button_eyedropper.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+
+let {EyedropperManager} = require("devtools/eyedropper/eyedropper");
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the eyedropper button");
+ testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function testButton(toolbox, Telemetry) {
+ let button = toolbox.doc.querySelector("#command-button-eyedropper");
+ ok(button, "Captain, we have the eyedropper button");
+
+ info("clicking the button to open the eyedropper");
+ button.click();
+
+ checkResults("_EYEDROPPER_", Telemetry);
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.contains(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ is(value.length, 1, histId + " has one entry");
+
+ let okay = value.every(element => element === true);
+ ok(okay, "All " + histId + " entries are === true");
+ }
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_telemetry_button_paintflashing.js b/toolkit/devtools/shared/test/browser_telemetry_button_paintflashing.js
new file mode 100644
index 000000000..692df44c4
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_button_paintflashing.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_paintflashing.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the paintflashing button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-paintflashing");
+
+ let button = toolbox.doc.querySelector("#command-button-paintflashing");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+ checkResults("_PAINTFLASHING_", Telemetry);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.contains(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_telemetry_button_responsive.js b/toolkit/devtools/shared/test/browser_telemetry_button_responsive.js
new file mode 100644
index 000000000..fd9a33460
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_button_responsive.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_responsive.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the responsivedesign button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-responsive");
+
+ let button = toolbox.doc.querySelector("#command-button-responsive");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+ checkResults("_RESPONSIVE_", Telemetry);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.contains(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_telemetry_button_scratchpad.js b/toolkit/devtools/shared/test/browser_telemetry_button_scratchpad.js
new file mode 100644
index 000000000..f96755fcf
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_button_scratchpad.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_scratchpad.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ let onAllWindowsOpened = trackScratchpadWindows();
+
+ info("testing the scratchpad button");
+ yield testButton(toolbox, Telemetry);
+ yield onAllWindowsOpened;
+
+ checkResults("_SCRATCHPAD_", Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function trackScratchpadWindows() {
+ info("register the window observer to track when scratchpad windows open");
+
+ let numScratchpads = 0;
+
+ return new Promise(resolve => {
+ Services.ww.registerNotification(function observer(subject, topic) {
+ if (topic == "domwindowopened") {
+ let win = subject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ if (win.Scratchpad) {
+ win.Scratchpad.addObserver({
+ onReady: function() {
+ win.Scratchpad.removeObserver(this);
+ numScratchpads++;
+ win.close();
+
+ info("another scratchpad was opened and closed, count is now " + numScratchpads);
+
+ if (numScratchpads === 4) {
+ Services.ww.unregisterNotification(observer);
+ info("4 scratchpads have been opened and closed, checking results");
+ resolve();
+ }
+ },
+ });
+ }
+ }, false);
+ }
+ });
+ });
+}
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-scratchpad");
+ let button = toolbox.doc.querySelector("#command-button-scratchpad");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.contains(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_telemetry_button_tilt.js b/toolkit/devtools/shared/test/browser_telemetry_button_tilt.js
new file mode 100644
index 000000000..4e36618ca
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_button_tilt.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_button_tilt.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ info("testing the tilt button");
+ yield testButton(toolbox, Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testButton(toolbox, Telemetry) {
+ info("Testing command-button-tilt");
+
+ let button = toolbox.doc.querySelector("#command-button-tilt");
+ ok(button, "Captain, we have the button");
+
+ yield delayedClicks(button, 4)
+ checkResults("_TILT_", Telemetry);
+}
+
+function delayedClicks(node, clicks) {
+ return new Promise(resolve => {
+ let clicked = 0;
+
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function delayedClick() {
+ info("Clicking button " + node.id);
+ node.click();
+ clicked++;
+
+ if (clicked >= clicks) {
+ resolve(node);
+ } else {
+ setTimeout(delayedClick, TOOL_DELAY);
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(histIdFocus, Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+ !histId.contains(histIdFocus)) {
+ // Inspector stats are tested in
+ // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+ // because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_telemetry_sidebar.js b/toolkit/devtools/shared/test/browser_telemetry_sidebar.js
new file mode 100644
index 000000000..b80930e0e
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_sidebar.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_sidebar.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, "inspector");
+ info("inspector opened");
+
+ yield testSidebar(toolbox);
+ checkResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ yield gDevTools.closeToolbox(target);
+ gBrowser.removeCurrentTab();
+});
+
+function* testSidebar(toolbox) {
+ info("Testing sidebar");
+
+ let inspector = toolbox.getCurrentPanel();
+ let sidebarTools = ["ruleview", "computedview", "fontinspector",
+ "layoutview", "animationinspector"];
+
+ // Concatenate the array with itself so that we can open each tool twice.
+ sidebarTools.push.apply(sidebarTools, sidebarTools);
+
+ return new Promise(resolve => {
+ // See TOOL_DELAY for why we need setTimeout here
+ setTimeout(function selectSidebarTab() {
+ let tool = sidebarTools.pop();
+ if (tool) {
+ inspector.sidebar.select(tool);
+ setTimeout(function() {
+ setTimeout(selectSidebarTab, TOOL_DELAY);
+ }, TOOL_DELAY);
+ } else {
+ resolve();
+ }
+ }, TOOL_DELAY);
+ });
+}
+
+function checkResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.startsWith("DEVTOOLS_INSPECTOR_")) {
+ // Inspector stats are tested in browser_telemetry_toolboxtabs.js so we
+ // skip them here because we only open the inspector once for this test.
+ continue;
+ }
+
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId === "DEVTOOLS_TOOLBOX_OPENED_BOOLEAN") {
+ is(value.length, 1, histId + " has only one entry");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolbox.js b/toolkit/devtools/shared/test/browser_telemetry_toolbox.js
new file mode 100644
index 000000000..366f64699
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolbox.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolbox.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(3, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
new file mode 100644
index 000000000..c9c07099a
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_canvasdebugger.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_canvasdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ info("Activate the canvasdebugger");
+ let originalPref = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true);
+
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "canvasdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the canvasdebugger");
+ Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", originalPref);
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
new file mode 100644
index 000000000..61dff980a
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_inspector.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_inspector.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "inspector");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
new file mode 100644
index 000000000..a86255183
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsdebugger.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_telemetry_toolboxtabs_jsdebugger.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "jsdebugger");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
new file mode 100644
index 000000000..afe2aeb29
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_jsprofiler.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_jsprofiler.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "jsprofiler");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
new file mode 100644
index 000000000..e7554e549
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_netmonitor.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_netmonitor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "netmonitor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_options.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
new file mode 100644
index 000000000..14256814e
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_options.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_options.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "options");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_shadereditor.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
new file mode 100644
index 000000000..476fbd320
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_shadereditor.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_shadereditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ info("Active the sharer editor");
+ let originalPref = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
+ Services.prefs.setBoolPref("devtools.shadereditor.enabled", true);
+
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "shadereditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activate the sharer editor");
+ Services.prefs.setBoolPref("devtools.shadereditor.enabled", originalPref);
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js
new file mode 100644
index 000000000..93348b96d
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_storage.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_storage.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ info("Activating the storage inspector");
+ Services.prefs.setBoolPref("devtools.storage.enabled", true);
+
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "storage");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the storage inspector");
+ Services.prefs.clearUserPref("devtools.storage.enabled");
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
new file mode 100644
index 000000000..15c4a9c08
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_styleeditor.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_styleeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "styleeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
new file mode 100644
index 000000000..033791a72
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webaudioeditor.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_webaudioeditor.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ info("Activating the webaudioeditor");
+ let originalPref = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
+
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webaudioeditor");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+
+ info("De-activating the webaudioeditor");
+ Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", originalPref);
+});
diff --git a/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
new file mode 100644
index 000000000..b989a1426
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_telemetry_toolboxtabs_webconsole.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolboxtabs_styleeditor_webconsole.js</p>";
+
+// Because we need to gather stats for the period of time that a tool has been
+// opened we make use of setTimeout() to create tool active times.
+const TOOL_DELAY = 200;
+
+add_task(function*() {
+ yield promiseTab(TEST_URI);
+ let Telemetry = loadTelemetryAndRecordLogs();
+
+ yield openAndCloseToolbox(2, TOOL_DELAY, "webconsole");
+ checkTelemetryResults(Telemetry);
+
+ stopRecordingTelemetryLogs(Telemetry);
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/devtools/shared/test/browser_templater_basic.html b/toolkit/devtools/shared/test/browser_templater_basic.html
new file mode 100644
index 000000000..473c731f3
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_templater_basic.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <title>DOM Template Tests</title>
+</head>
+<body>
+
+</body>
+</html>
+
diff --git a/toolkit/devtools/shared/test/browser_templater_basic.js b/toolkit/devtools/shared/test/browser_templater_basic.js
new file mode 100644
index 000000000..03ca1657c
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_templater_basic.js
@@ -0,0 +1,285 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the DOM Template engine works properly
+
+/*
+ * These tests run both in Mozilla/Mochitest and plain browsers (as does
+ * domtemplate)
+ * We should endevour to keep the source in sync.
+ */
+
+const template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
+
+const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
+
+let test = Task.async(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ info("Starting DOM Templater Tests");
+ runTest(0, host, doc);
+});
+
+function runTest(index, host, doc) {
+ var options = tests[index] = tests[index]();
+ var holder = doc.createElement('div');
+ holder.id = options.name;
+ var body = doc.body;
+ body.appendChild(holder);
+ holder.innerHTML = options.template;
+
+ info('Running ' + options.name);
+ template(holder, options.data, options.options);
+
+ if (typeof options.result == 'string') {
+ is(holder.innerHTML, options.result, options.name);
+ }
+ else {
+ ok(holder.innerHTML.match(options.result) != null,
+ options.name + ' result=\'' + holder.innerHTML + '\'');
+ }
+
+ if (options.also) {
+ options.also(options);
+ }
+
+ function runNextTest() {
+ index++;
+ if (index < tests.length) {
+ runTest(index, host, doc);
+ }
+ else {
+ finished(host);
+ }
+ }
+
+ if (options.later) {
+ var ais = is.bind(this);
+
+ function createTester(holder, options) {
+ return () => {
+ ais(holder.innerHTML, options.later, options.name + ' later');
+ runNextTest();
+ };
+ }
+
+ executeSoon(createTester(holder, options));
+ }
+ else {
+ runNextTest();
+ }
+}
+
+function finished(host) {
+ host.destroy();
+ gBrowser.removeCurrentTab();
+ info("Finishing DOM Templater Tests");
+ tests = null;
+ finish();
+}
+
+/**
+ * Why have an array of functions that return data rather than just an array
+ * of the data itself? Some of these tests contain calls to delayReply() which
+ * sets up async processing using executeSoon(). Since the execution of these
+ * tests is asynchronous, the delayed reply will probably arrive before the
+ * test is executed, making the test be synchronous. So we wrap the data in a
+ * function so we only set it up just before we use it.
+ */
+var tests = [
+ function() { return {
+ name: 'simpleNesting',
+ template: '<div id="ex1">${nested.value}</div>',
+ data: { nested:{ value:'pass 1' } },
+ result: '<div id="ex1">pass 1</div>'
+ };},
+
+ function() { return {
+ name: 'returnDom',
+ template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+ options: { allowEval: true },
+ data: {},
+ result: '<div id="ex2">pass 2</div>'
+ };},
+
+ function() { return {
+ name: 'srcChange',
+ template: '<img _src="${fred}" id="ex3">',
+ data: { fred:'green.png' },
+ result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
+ };},
+
+ function() { return {
+ name: 'ifTrue',
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: 'fred' },
+ result: '<p>hello fred</p>'
+ };},
+
+ function() { return {
+ name: 'ifFalse',
+ template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+ options: { allowEval: true },
+ data: { name: 'jim' },
+ result: ''
+ };},
+
+ function() { return {
+ name: 'simpleLoop',
+ template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+ options: { allowEval: true },
+ data: {},
+ result: '<p>1</p><p>2</p><p>3</p>'
+ };},
+
+ function() { return {
+ name: 'loopElement',
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: [ 1, 2, 3 ] },
+ result: '123'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ // Bug 692031: DOMTemplate async loops do not drop the loop element
+ function() { return {
+ name: 'asyncLoopElement',
+ template: '<loop foreach="i in ${array}">${i}</loop>',
+ data: { array: delayReply([1, 2, 3]) },
+ result: '<span></span>',
+ later: '123'
+ };},
+
+ function() { return {
+ name: 'saveElement',
+ template: '<p save="${element}">${name}</p>',
+ data: { name: 'pass 8' },
+ result: '<p>pass 8</p>',
+ also: function(options) {
+ ok(options.data.element.innerHTML, 'pass 9', 'saveElement saved');
+ delete options.data.element;
+ }
+ };},
+
+ function() { return {
+ name: 'useElement',
+ template: '<p id="pass9">${adjust(__element)}</p>',
+ options: { allowEval: true },
+ data: {
+ adjust: function(element) {
+ is('pass9', element.id, 'useElement adjust');
+ return 'pass 9b'
+ }
+ },
+ result: '<p id="pass9">pass 9b</p>'
+ };},
+
+ function() { return {
+ name: 'asyncInline',
+ template: '${delayed}',
+ data: { delayed: delayReply('inline') },
+ result: '<span></span>',
+ later: 'inline'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ function() { return {
+ name: 'asyncArray',
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: delayReply([1, 2, 3]) },
+ result: '<span></span>',
+ later: '<p>1</p><p>2</p><p>3</p>'
+ };},
+
+ function() { return {
+ name: 'asyncMember',
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
+ result: '<span></span><span></span><span></span>',
+ later: '<p>4</p><p>5</p><p>6</p>'
+ };},
+
+ // Bug 692028: DOMTemplate memory leak with asynchronous arrays
+ function() { return {
+ name: 'asyncBoth',
+ template: '<p foreach="i in ${delayed}">${i}</p>',
+ data: {
+ delayed: delayReply([
+ delayReply(4),
+ delayReply(5),
+ delayReply(6)
+ ])
+ },
+ result: '<span></span>',
+ later: '<p>4</p><p>5</p><p>6</p>'
+ };},
+
+ // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+ function() { return {
+ name: 'functionReturningUndefiend',
+ template: '<p>${foo()}</p>',
+ options: { allowEval: true },
+ data: {
+ foo: function() {}
+ },
+ result: '<p>undefined</p>'
+ };},
+
+ // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+ function() { return {
+ name: 'propertySimple',
+ template: '<p>${a.b.c}</p>',
+ data: { a: { b: { c: 'hello' } } },
+ result: '<p>hello</p>'
+ };},
+
+ function() { return {
+ name: 'propertyPass',
+ template: '<p>${Math.max(1, 2)}</p>',
+ options: { allowEval: true },
+ result: '<p>2</p>'
+ };},
+
+ function() { return {
+ name: 'propertyFail',
+ template: '<p>${Math.max(1, 2)}</p>',
+ result: '<p>${Math.max(1, 2)}</p>'
+ };},
+
+ // Bug 723431: DOMTemplate should allow customisation of display of
+ // null/undefined values
+ function() { return {
+ name: 'propertyUndefAttrFull',
+ template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<p>null|undefined|undefined</p>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrBlank',
+ template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<p>||</p>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrFull',
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ result: '<div><p value="null"></p><p value="undefined"></p><p value="undefined"></p></div>'
+ };},
+
+ function() { return {
+ name: 'propertyUndefAttrBlank',
+ template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
+ data: { nullvar: null, undefinedvar1: undefined },
+ options: { blankNullUndefined: true },
+ result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
+ };}
+];
+
+function delayReply(data) {
+ return new Promise(resolve => resolve(data));
+}
diff --git a/toolkit/devtools/shared/test/browser_theme.js b/toolkit/devtools/shared/test/browser_theme.js
new file mode 100644
index 000000000..632e4538f
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_theme.js
@@ -0,0 +1,81 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that theme utilities work
+
+let {getColor, getTheme, setTheme} = devtools.require("devtools/shared/theme");
+
+function test() {
+ testGetTheme();
+ testSetTheme();
+ testGetColor();
+ testColorExistence();
+}
+
+function testGetTheme () {
+ let originalTheme = getTheme();
+ ok(originalTheme, "has some theme to start with.");
+ Services.prefs.setCharPref("devtools.theme", "light");
+ is(getTheme(), "light", "getTheme() correctly returns light theme");
+ Services.prefs.setCharPref("devtools.theme", "dark");
+ is(getTheme(), "dark", "getTheme() correctly returns dark theme");
+ Services.prefs.setCharPref("devtools.theme", "unknown");
+ is(getTheme(), "unknown", "getTheme() correctly returns an unknown theme");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testSetTheme () {
+ let originalTheme = getTheme();
+ setTheme("dark");
+ is(Services.prefs.getCharPref("devtools.theme"), "dark", "setTheme() correctly sets dark theme.");
+ setTheme("light");
+ is(Services.prefs.getCharPref("devtools.theme"), "light", "setTheme() correctly sets light theme.");
+ setTheme("unknown");
+ is(Services.prefs.getCharPref("devtools.theme"), "unknown", "setTheme() correctly sets an unknown theme.");
+ Services.prefs.setCharPref("devtools.theme", originalTheme);
+}
+
+function testGetColor () {
+ let BLUE_DARK = "#3689b2";
+ let BLUE_LIGHT = "hsl(208,56%,40%)";
+ let originalTheme = getTheme();
+
+ setTheme("dark");
+ is(getColor("highlight-blue"), BLUE_DARK, "correctly gets color for enabled theme.");
+ setTheme("light");
+ is(getColor("highlight-blue"), BLUE_LIGHT, "correctly gets color for enabled theme.");
+ setTheme("metal");
+ is(getColor("highlight-blue"), BLUE_LIGHT, "correctly uses light for default theme if enabled theme not found");
+
+ is(getColor("highlight-blue", "dark"), BLUE_DARK, "if provided and found, uses the provided theme.");
+ is(getColor("highlight-blue", "metal"), BLUE_LIGHT, "if provided and not found, defaults to light theme.");
+ is(getColor("somecomponents"), null, "if a type cannot be found, should return null.");
+
+ setTheme(originalTheme);
+}
+
+function testColorExistence () {
+ var vars = ["body-background", "sidebar-background", "contrast-background", "tab-toolbar-background",
+ "toolbar-background", "selection-background", "selection-color",
+ "selection-background-semitransparent", "splitter-color", "comment", "body-color",
+ "body-color-alt", "content-color1", "content-color2", "content-color3",
+ "highlight-green", "highlight-blue", "highlight-bluegrey", "highlight-purple",
+ "highlight-lightorange", "highlight-orange", "highlight-red", "highlight-pink"
+ ];
+
+ for (let type of vars) {
+ ok(getColor(type, "light"), `${type} is a valid color in light theme`);
+ ok(getColor(type, "dark"), `${type} is a valid color in light theme`);
+ }
+}
+
+function isColor (s) {
+ // Regexes from Heather Arthur's `color-string`
+ // https://github.com/harthur/color-string
+ // MIT License
+ return /^#([a-fA-F0-9]{3})$/.test(s) ||
+ /^#([a-fA-F0-9]{6})$/.test(s) ||
+ /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d\.]+)\s*)?\)$/.test(s) ||
+ /^rgba?\(\s*([\d\.]+)\%\s*,\s*([\d\.]+)\%\s*,\s*([\d\.]+)\%\s*(?:,\s*([\d\.]+)\s*)?\)$/.test(s);
+}
diff --git a/toolkit/devtools/shared/test/browser_toolbar_basic.html b/toolkit/devtools/shared/test/browser_toolbar_basic.html
new file mode 100644
index 000000000..7ec012b0e
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_toolbar_basic.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests</title>
+ <style type="text/css">
+ #single { color: red; }
+ </style>
+ <script type="text/javascript">var a=1;</script>
+</head>
+<body>
+
+<p id=single>
+1
+</p>
+
+<p class=twin>
+2a
+</p>
+
+<p class=twin>
+2b
+</p>
+
+<style>
+.twin { color: blue; }
+</style>
+<script>var b=2;</script>
+
+</body>
+</html>
+
diff --git a/toolkit/devtools/shared/test/browser_toolbar_basic.js b/toolkit/devtools/shared/test/browser_toolbar_basic.js
new file mode 100644
index 000000000..cfeb7f958
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_toolbar_basic.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
+
+function test() {
+ addTab(TEST_URI, function(browser, tab) {
+ info("Starting browser_toolbar_basic.js");
+ runTest();
+ });
+}
+
+function runTest() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(checkOpen));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function isChecked(b) {
+ return b.getAttribute("checked") == "true";
+}
+
+function checkOpen() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkOpen");
+ let close = document.getElementById("developer-toolbar-closebutton");
+ ok(close, "Close button exists");
+
+ let toggleToolbox =
+ document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ addTab("about:blank", function(browser, tab) {
+ info("Opened a new tab");
+
+ ok(!isChecked(toggleToolbox), "toggle toolbox button is not checked");
+
+ gBrowser.removeCurrentTab();
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE, catchFail(checkClosed));
+ document.getElementById("Tools:DevToolbar").doCommand();
+ });
+ });
+}
+
+function checkClosed() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in checkClosed");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.SHOW, catchFail(checkReOpen));
+ document.getElementById("Tools:DevToolbar").doCommand();
+}
+
+function checkReOpen() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible in checkReOpen");
+
+ let toggleToolbox =
+ document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+ ok(isChecked(toggleToolbox), "toggle toolbox button is checked");
+
+ oneTimeObserve(DeveloperToolbar.NOTIFICATIONS.HIDE, catchFail(checkReClosed));
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+}
+
+function checkReClosed() {
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in checkReClosed");
+
+ finish();
+}
diff --git a/toolkit/devtools/shared/test/browser_toolbar_tooltip.js b/toolkit/devtools/shared/test/browser_toolbar_tooltip.js
new file mode 100644
index 000000000..a62996d61
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_toolbar_tooltip.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar works properly
+
+///////////////////
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Protocol error (unknownError): Error: Got an invalid root window in DocumentWalker");
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>Tooltip Tests</p>";
+
+function test() {
+ addTab(TEST_URI, function() {
+ Task.spawn(runTest).catch(err => {
+ ok(false, ex);
+ console.error(ex);
+ }).then(finish);
+ });
+}
+
+function* runTest() {
+ info("Starting browser_toolbar_tooltip.js");
+
+ ok(!DeveloperToolbar.visible, "DeveloperToolbar is not visible in runTest");
+
+ let showPromise = observeOnce(DeveloperToolbar.NOTIFICATIONS.SHOW);
+ document.getElementById("Tools:DevToolbar").doCommand();
+ yield showPromise;
+
+ let tooltipPanel = DeveloperToolbar.tooltipPanel;
+
+ DeveloperToolbar.display.focusManager.helpRequest();
+ yield DeveloperToolbar.display.inputter.setInput('help help');
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length });
+ is(tooltipPanel._dimensions.start, 'help '.length,
+ 'search param start, when cursor at end');
+ ok(getLeftMargin() > 30, 'tooltip offset, when cursor at end')
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 'help'.length });
+ is(tooltipPanel._dimensions.start, 0,
+ 'search param start, when cursor at end of command');
+ ok(getLeftMargin() > 9, 'tooltip offset, when cursor at end of command')
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 'help help'.length - 1 });
+ is(tooltipPanel._dimensions.start, 'help '.length,
+ 'search param start, when cursor at penultimate position');
+ ok(getLeftMargin() > 30, 'tooltip offset, when cursor at penultimate position')
+
+ DeveloperToolbar.display.inputter.setCursor({ start: 0 });
+ is(tooltipPanel._dimensions.start, 0,
+ 'search param start, when cursor at start');
+ ok(getLeftMargin() > 9, 'tooltip offset, when cursor at start')
+}
+
+function getLeftMargin() {
+ let style = DeveloperToolbar.tooltipPanel._panel.style.marginLeft;
+ return parseInt(style.slice(0, -2), 10);
+}
+
+function observeOnce(topic, ownsWeak=false) {
+ return new Promise(function(resolve, reject) {
+ let resolver = function(subject) {
+ Services.obs.removeObserver(resolver, topic);
+ resolve(subject);
+ };
+ Services.obs.addObserver(resolver, topic, ownsWeak);
+ }.bind(this));
+}
diff --git a/toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.html b/toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.html
new file mode 100644
index 000000000..216cc0d49
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Developer Toolbar Tests - errors count in the Web Console button</title>
+ <script type="text/javascript">
+ console.log("foobarBug762996consoleLog");
+ window.onload = function() {
+ window.foobarBug762996load();
+ };
+ window.foobarBug762996a();
+ </script>
+ <script type="text/javascript">
+ window.foobarBug762996b();
+ </script>
+</head>
+<body>
+ <p>Hello world! Test for errors count in the Web Console button (developer
+ toolbar).</p>
+ <p style="color: foobarBug762996css"><button>click me</button></p>
+ <script type="text/javascript;version=1.8">
+ "use strict";
+ let testObj = {};
+ document.querySelector("button").onclick = function() {
+ let test = testObj.fooBug788445 + "warning";
+ window.foobarBug762996click();
+ };
+ </script>
+</body>
+</html>
diff --git a/toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.js b/toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
new file mode 100644
index 000000000..8d6ed78bb
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_toolbar_webconsole_errors_count.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the developer toolbar errors count works properly.
+
+function test() {
+ const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
+
+ let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
+ {}).gDevTools;
+
+ let webconsole = document.getElementById("developer-toolbar-toolbox-button");
+ let tab1, tab2;
+
+ Services.prefs.setBoolPref("javascript.options.strict", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("javascript.options.strict");
+ });
+
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI, openToolbar);
+
+ function openToolbar(browser, tab) {
+ tab1 = tab;
+ ignoreAllUncaughtExceptions(false);
+
+ expectUncaughtException();
+
+ if (!DeveloperToolbar.visible) {
+ DeveloperToolbar.show(true, onOpenToolbar);
+ }
+ else {
+ onOpenToolbar();
+ }
+ }
+
+ function onOpenToolbar() {
+ ok(DeveloperToolbar.visible, "DeveloperToolbar is visible");
+
+ waitForButtonUpdate({
+ name: "web console button shows page errors",
+ errors: 3,
+ warnings: 0,
+ callback: addErrors,
+ });
+ }
+
+ function addErrors() {
+ expectUncaughtException();
+
+ waitForFocus(function() {
+ let button = content.document.querySelector("button");
+ executeSoon(function() {
+ EventUtils.synthesizeMouse(button, 3, 2, {}, content);
+ });
+ }, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after click in page",
+ errors: 4,
+ warnings: 1,
+ callback: () => {
+ ignoreAllUncaughtExceptions();
+ addTab(TEST_URI, onOpenSecondTab);
+ },
+ });
+ }
+
+ function onOpenSecondTab(browser, tab) {
+ tab2 = tab;
+
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "button shows correct number of errors after new tab is open",
+ errors: 3,
+ warnings: 0,
+ callback: switchToTab1,
+ });
+ }
+
+ function switchToTab1() {
+ gBrowser.selectedTab = tab1;
+ waitForButtonUpdate({
+ name: "button shows the page errors from tab 1",
+ errors: 4,
+ warnings: 1,
+ callback: openWebConsole.bind(null, tab1, onWebConsoleOpen),
+ });
+ }
+
+ function onWebConsoleOpen(hud) {
+ dump("lolz!!\n");
+ waitForValue({
+ name: "web console shows the page errors",
+ validator: function() {
+ return hud.outputNode.querySelectorAll(".message[category=exception][severity=error]").length;
+ },
+ value: 4,
+ success: checkConsoleOutput.bind(null, hud),
+ failure: () => {
+ finish();
+ },
+ });
+ }
+
+ function checkConsoleOutput(hud) {
+ let msgs = ["foobarBug762996a", "foobarBug762996b", "foobarBug762996load",
+ "foobarBug762996click", "foobarBug762996consoleLog",
+ "foobarBug762996css", "fooBug788445"];
+ msgs.forEach(function(msg) {
+ isnot(hud.outputNode.textContent.indexOf(msg), -1,
+ msg + " found in the Web Console output");
+ });
+
+ hud.jsterm.clearOutput();
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996color"), -1,
+ "clearOutput() worked");
+
+ expectUncaughtException();
+ let button = content.document.querySelector("button");
+ EventUtils.synthesizeMouse(button, 2, 2, {}, content);
+
+ waitForButtonUpdate({
+ name: "button shows one more error after another click in page",
+ errors: 5,
+ warnings: 1, // warnings are not repeated by the js engine
+ callback: () => waitForValue(waitForNewError),
+ });
+
+ let waitForNewError = {
+ name: "the Web Console displays the new error",
+ validator: function() {
+ return hud.outputNode.textContent.indexOf("foobarBug762996click") > -1;
+ },
+ success: doClearConsoleButton.bind(null, hud),
+ failure: finish,
+ };
+ }
+
+ function doClearConsoleButton(hud) {
+ let clearButton = hud.ui.rootElement
+ .querySelector(".webconsole-clear-console-button");
+ EventUtils.synthesizeMouse(clearButton, 2, 2, {}, hud.iframeWindow);
+
+ is(hud.outputNode.textContent.indexOf("foobarBug762996click"), -1,
+ "clear console button worked");
+ is(getErrorsCount(), 0, "page errors counter has been reset");
+ let tooltip = getTooltipValues();
+ is(tooltip[1], 0, "page warnings counter has been reset");
+
+ doPageReload(hud);
+ }
+
+ function doPageReload(hud) {
+ tab1.linkedBrowser.addEventListener("load", onReload, true);
+
+ ignoreAllUncaughtExceptions();
+ content.location.reload();
+
+ function onReload() {
+ tab1.linkedBrowser.removeEventListener("load", onReload, true);
+ ignoreAllUncaughtExceptions(false);
+ expectUncaughtException();
+
+ waitForButtonUpdate({
+ name: "the Web Console button count has been reset after page reload",
+ errors: 3,
+ warnings: 0,
+ callback: waitForValue.bind(null, waitForConsoleOutputAfterReload),
+ });
+ }
+
+ let waitForConsoleOutputAfterReload = {
+ name: "the Web Console displays the correct number of errors after reload",
+ validator: function() {
+ return hud.outputNode.querySelectorAll(".message[category=exception][severity=error]").length;
+ },
+ value: 3,
+ success: function() {
+ isnot(hud.outputNode.textContent.indexOf("foobarBug762996load"), -1,
+ "foobarBug762996load found in console output after page reload");
+ testEnd();
+ },
+ failure: testEnd,
+ };
+ }
+
+ function testEnd() {
+ document.getElementById("developer-toolbar-closebutton").doCommand();
+ let target1 = TargetFactory.forTab(tab1);
+ gDevTools.closeToolbox(target1).then(() => {
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ });
+ }
+
+ // Utility functions
+
+ function getErrorsCount() {
+ let count = webconsole.getAttribute("error-count");
+ return count ? count : "0";
+ }
+
+ function getTooltipValues() {
+ let matches = webconsole.getAttribute("tooltiptext")
+ .match(/(\d+) errors?, (\d+) warnings?/);
+ return matches ? [matches[1], matches[2]] : [0, 0];
+ }
+
+ function waitForButtonUpdate(options) {
+ function check() {
+ let errors = getErrorsCount();
+ let tooltip = getTooltipValues();
+ let result = errors == options.errors && tooltip[1] == options.warnings;
+ if (result) {
+ ok(true, options.name);
+ is(errors, tooltip[0], "button error-count is the same as in the tooltip");
+
+ // Get out of the toolbar event execution loop.
+ executeSoon(options.callback);
+ }
+ return result;
+ }
+
+ if (!check()) {
+ info("wait for: " + options.name);
+ DeveloperToolbar.on("errors-counter-updated", function onUpdate(event) {
+ if (check()) {
+ DeveloperToolbar.off(event, onUpdate);
+ }
+ });
+ }
+ }
+
+ function openWebConsole(tab, callback)
+ {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target, "webconsole").then((toolbox) =>
+ callback(toolbox.getCurrentPanel().hud));
+ }
+}
diff --git a/toolkit/devtools/shared/test/browser_treeWidget_basic.js b/toolkit/devtools/shared/test/browser_treeWidget_basic.js
new file mode 100644
index 000000000..170c75cf7
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_treeWidget_basic.js
@@ -0,0 +1,254 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the tree widget api works fine
+
+const TEST_URI = "data:text/html;charset=utf-8,<head><link rel='stylesheet' " +
+ "type='text/css' href='chrome://browser/skin/devtools/common.css'><link " +
+ "rel='stylesheet' type='text/css' href='chrome://browser/skin/devtools/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = devtools.require("devtools/shared/widgets/TreeWidget");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ testTreeItemInsertedCorrectly(tree, doc);
+ testAPI(tree, doc);
+ populateUnsortedTree(tree, doc);
+ testUnsortedTreeItemInsertedCorrectly(tree, doc);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the tree.
+ */
+function testTreeItemInsertedCorrectly(tree, doc) {
+ is(tree.root.children.children.length, 2, "Number of top level elements match");
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of first second level elements match");
+ is(tree.root.children.lastChild.lastChild.children.length, 1,
+ "Number of second second level elements match");
+
+ ok(tree.root.items.has("level1"), "Level1 top level element exists");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1"]),
+ "Data id of first top level element matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "Level 1",
+ "Text content of first top level element matches");
+
+ ok(tree.root.items.has("level1.1"), "Level1.1 top level element exists");
+ is(tree.root.children.firstChild.nextSibling.dataset.id,
+ JSON.stringify(["level1.1"]),
+ "Data id of second top level element matches");
+ is(tree.root.children.firstChild.nextSibling.firstChild.textContent, "level1.1",
+ "Text content of second top level element matches");
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+
+ is(tree.root.children.children.length, 3,
+ "Number of top level elements match after update");
+ ok(tree.root.items.has("level1.2"), "New level node got added");
+ ok(tree.attachments.has(JSON.stringify(["level1.2"])),
+ "Attachment is present for newly added node");
+ // The item should be added before level1 and level 1.1 as lexical sorting
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["level1.2"]),
+ "Data id of last top level element matches");
+ is(tree.root.children.firstChild.firstChild.firstChild, node,
+ "Newly added node is inserted at the right location");
+}
+
+/**
+ * Populate the unsorted tree.
+ */
+function populateUnsortedTree(tree, doc) {
+ tree.sorted = false;
+
+ tree.add([{ id: "g-1", label: "g-1"}])
+ tree.add(["g-1", { id: "d-2", label: "d-2.1"}]);
+ tree.add(["g-1", { id: "b-2", label: "b-2.2"}]);
+ tree.add(["g-1", { id: "a-2", label: "a-2.3"}]);
+}
+
+/**
+ * Test if the nodes are inserted correctly in the unsorted tree.
+ */
+function testUnsortedTreeItemInsertedCorrectly(tree, doc) {
+ ok(tree.root.items.has("g-1"), "g-1 top level element exists");
+
+ is(tree.root.children.firstChild.lastChild.children.length, 3,
+ "Number of children for g-1 matches");
+ is(tree.root.children.firstChild.dataset.id, JSON.stringify(["g-1"]),
+ "Data id of g-1 matches");
+ is(tree.root.children.firstChild.firstChild.textContent, "g-1",
+ "Text content of g-1 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.dataset.id,
+ JSON.stringify(["g-1", "d-2"]),
+ "Data id of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.textContent, "d-2.1",
+ "Text content of d-2 matches");
+ is(tree.root.children.firstChild.lastChild.firstChild.nextSibling.textContent,
+ "b-2.2", "Text content of b-2 matches");
+ is(tree.root.children.firstChild.lastChild.lastChild.textContent, "a-2.3",
+ "Text content of a-2 matches");
+}
+
+/**
+ * Tests if the API exposed by TreeWidget works properly
+ */
+function testAPI(tree, doc) {
+ info("Testing TreeWidget API");
+ // Check if selectItem and selectedItem setter works as expected
+ // Nothing should be selected beforehand
+ ok(!doc.querySelector(".theme-selected"), "Nothing is selected");
+ tree.selectItem(["level1"]);
+ let node = doc.querySelector(".theme-selected");
+ ok(!!node, "Something got selected");
+ is(node.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ tree.selectItem(["level1", "level2"]);
+ let node2 = doc.querySelector(".theme-selected");
+ ok(!!node2, "Something is still selected");
+ isnot(node, node2, "Newly selected node is different from previous");
+ is(node2.parentNode.dataset.id, JSON.stringify(["level1", "level2"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 2, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+ is(tree.selectedItem[1], "level2", "Correct selected item");
+
+ // test if isSelected works
+ ok(tree.isSelected(["level1", "level2"]), "isSelected works");
+
+ tree.selectedItem = ["level1"];
+ let node3 = doc.querySelector(".theme-selected");
+ ok(!!node3, "Something is still selected");
+ isnot(node2, node3, "Newly selected node is different from previous");
+ is(node3, node, "First and third selected nodes should be same");
+ is(node3.parentNode.dataset.id, JSON.stringify(["level1"]),
+ "Correct node selected");
+
+ // test if selectedItem getter works
+ is(tree.selectedItem.length, 1, "Correct length of selected item");
+ is(tree.selectedItem[0], "level1", "Correct selected item");
+
+ // test if clear selection works
+ tree.clearSelection();
+ ok(!doc.querySelector(".theme-selected"),
+ "Nothing selected after clear selection call")
+
+ // test if collapseAll/expandAll work
+ ok(doc.querySelectorAll("[expanded]").length > 0,
+ "Some nodes are expanded");
+ tree.collapseAll();
+ is(doc.querySelectorAll("[expanded]").length, 0,
+ "Nothing is expanded after collapseAll call");
+ tree.expandAll();
+ is(doc.querySelectorAll("[expanded]").length, 13,
+ "All tree items expanded after expandAll call");
+
+ // test if selectNextItem and selectPreviousItem work
+ tree.selectedItem = ["level1", "level2"];
+ ok(tree.isSelected(["level1", "level2"]), "Correct item selected");
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after second selectNextItem call");
+
+ tree.selectNextItem();
+ ok(tree.isSelected(["level1", "level2-1", "level3-1"]),
+ "Correct item selected after third selectNextItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2-1"]),
+ "Correct item selected after selectPreviousItem call");
+
+ tree.selectPreviousItem();
+ ok(tree.isSelected(["level1", "level2", "level3"]),
+ "Correct item selected after second selectPreviousItem call");
+
+ // test if remove works
+ ok(doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item exists before removing");
+ tree.remove(["level1", "level2", "level3"]);
+ ok(!doc.querySelector("[data-id='" +
+ JSON.stringify(["level1", "level2", "level3"]) + "']"),
+ "level1-level2-level3 item does not exist after removing");
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+
+ // test if clearing the tree works
+ is(doc.querySelectorAll("[level='1']").length, 3,
+ "Correct number of top level items before clearing");
+ tree.clear();
+ is(doc.querySelectorAll("[level='1']").length, 0,
+ "No top level item after clearing the tree");
+}
diff --git a/toolkit/devtools/shared/test/browser_treeWidget_keyboard_interaction.js b/toolkit/devtools/shared/test/browser_treeWidget_keyboard_interaction.js
new file mode 100644
index 000000000..c1a4a9055
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_treeWidget_keyboard_interaction.js
@@ -0,0 +1,228 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that keyboard interaction works fine with the tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head><link rel='stylesheet' " +
+ "type='text/css' href='chrome://browser/skin/devtools/common.css'><link " +
+ "rel='stylesheet' type='text/css' href='chrome://browser/skin/devtools/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = devtools.require("devtools/shared/widgets/TreeWidget");
+const {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testKeyboardInteraction(tree, win);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if pressing navigation keys on the tree items does the expected behavior
+ */
+function* testKeyboardInteraction(tree, win) {
+ info("Testing keyboard interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ info("clicking on first top level item");
+ let node = tree.root.children.firstChild.firstChild;
+ event = Promise.defer();
+ tree.once("select", pass);
+ click(node);
+ yield event.promise;
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ // node should not have selected class
+ ok(!node.classList.contains("theme-selected"), "Node should not have selected class");
+ ok(!node.hasAttribute("expanded"), "Node is not expanded");
+
+ info("Pressing down key to select next item");
+ event = Promise.defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ let [name, data, attachment] = yield event.promise;
+ is(name, "select", "Select event was fired after pressing down");
+ is(data[0], "level1", "Correct item was selected after pressing down");
+ ok(!attachment, "null attachment was emitted");
+ ok(node.classList.contains("theme-selected"), "Node has selected class");
+ ok(node.hasAttribute("expanded"), "Node is expanded now");
+
+ info("Pressing down key again to select next item");
+ event = Promise.defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after second down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+
+ info("Pressing down key again to select next item");
+ event = Promise.defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after third down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+
+ info("Pressing down key again to select next item");
+ event = Promise.defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fourth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // pressing left to check expand collapse feature.
+ // This does not emit any event, so listening for keypress
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ // executeSoon so that other listeners on the same method are executed first
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = Promise.defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+
+ ok(!node.hasAttribute("expanded"), "Item is not expanded after left keypress");
+
+ // pressing left on collapsed item should select the previous item
+
+ info("Pressing left key on collapsed item to select previous");
+ tree.once("select", pass);
+ event = Promise.defer();
+ // parent node should have no effect of this keypress
+ node = tree.root.children.firstChild.nextSibling.firstChild;
+ ok(node.hasAttribute("expanded"), "Parent is expanded");
+ EventUtils.sendKey("LEFT", win);
+ [name, data] = yield event.promise;
+ is(data.length, 3, "Correct level item was selected after second left keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2", "Correct second level");
+ is(data[2], "level3", "Correct third level");
+ ok(node.hasAttribute("expanded"), "Parent is still expanded after left keypress");
+
+ // pressing down again
+
+ info("Pressing down key to select next item");
+ event = Promise.defer();
+ tree.once("select", pass);
+ EventUtils.sendKey("DOWN", win);
+ [name, data, attachment] = yield event.promise;
+ is(data.length, 2, "Correct level item was selected after fifth down keypress");
+ is(data[0], "level1", "Correct parent level");
+ is(data[1], "level2-1", "Correct second level");
+
+ // collapsing the item to check expand feature.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing left key to collapse the item");
+ event = Promise.defer();
+ node = tree._selectedLabel;
+ ok(node.hasAttribute("expanded"), "Item is expanded before left keypress");
+ EventUtils.sendKey("LEFT", win);
+ yield event.promise;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed after left keypress");
+
+ // pressing right should expand this now.
+
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing right key to expend the collapsed item");
+ event = Promise.defer();
+ node = tree._selectedLabel;
+ ok(!node.hasAttribute("expanded"), "Item is collapsed before right keypress");
+ EventUtils.sendKey("RIGHT", win);
+ yield event.promise;
+ ok(node.hasAttribute("expanded"), "Item is expanded after right keypress");
+
+ // selecting last item node to test edge navigation case
+
+ tree.selectedItem = ["level1.1", "level2", "level3"];
+ node = tree._selectedLabel;
+ // pressing down again should not change selection
+ event = Promise.defer();
+ tree.root.children.addEventListener("keypress", function onClick() {
+ tree.root.children.removeEventListener("keypress", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ info("Pressing down key on last item of the tree");
+ EventUtils.sendKey("DOWN", win);
+ yield event.promise;
+
+ ok(tree.isSelected(["level1.1", "level2", "level3"]),
+ "Last item is still selected after pressing down on last item of the tree");
+}
diff --git a/toolkit/devtools/shared/test/browser_treeWidget_mouse_interaction.js b/toolkit/devtools/shared/test/browser_treeWidget_mouse_interaction.js
new file mode 100644
index 000000000..cf8829489
--- /dev/null
+++ b/toolkit/devtools/shared/test/browser_treeWidget_mouse_interaction.js
@@ -0,0 +1,137 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that mouse interaction works fine with tree widget
+
+const TEST_URI = "data:text/html;charset=utf-8,<head><link rel='stylesheet' " +
+ "type='text/css' href='chrome://browser/skin/devtools/common.css'><link " +
+ "rel='stylesheet' type='text/css' href='chrome://browser/skin/devtools/widg" +
+ "ets.css'></head><body><div></div><span></span></body>";
+const {TreeWidget} = devtools.require("devtools/shared/widgets/TreeWidget");
+const {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+ yield promiseTab("about:blank");
+ let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+ let tree = new TreeWidget(doc.querySelector("div"), {
+ defaultType: "store"
+ });
+
+ populateTree(tree, doc);
+ yield testMouseInteraction(tree);
+
+ tree.destroy();
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function populateTree(tree, doc) {
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2-1",
+ label: "Level 2"
+ }, {
+ id: "level3-1",
+ label: "Level 3 - Child 1",
+ type: "dir"
+ }]);
+ tree.add(["level1", "level2-1", { id: "level3-2", label: "Level 3 - Child 2"}]);
+ tree.add(["level1", "level2-1", { id: "level3-3", label: "Level 3 - Child 3"}]);
+ tree.add(["level1", {
+ id: "level2-2",
+ label: "Level 2.1"
+ }, {
+ id: "level3-1",
+ label: "Level 3.1"
+ }]);
+ tree.add([{
+ id: "level1",
+ label: "Level 1"
+ }, {
+ id: "level2",
+ label: "Level 2"
+ }, {
+ id: "level3",
+ label: "Level 3",
+ type: "js"
+ }]);
+ tree.add(["level1.1", "level2", {id: "level3", type: "url"}]);
+
+ // Adding a new non text item in the tree.
+ let node = doc.createElement("div");
+ node.textContent = "Foo Bar";
+ node.className = "foo bar";
+ tree.add([{
+ id: "level1.2",
+ node: node,
+ attachment: {
+ foo: "bar"
+ }
+ }]);
+}
+
+// Sends a click event on the passed DOM node in an async manner
+function click(node) {
+ let win = node.ownerDocument.defaultView;
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(node, {}, win));
+}
+
+/**
+ * Tests if clicking the tree items does the expected behavior
+ */
+function* testMouseInteraction(tree) {
+ info("Testing mouse interaction with the tree");
+ let event;
+ let pass = (e, d, a) => event.resolve([e, d, a]);
+
+ ok(!tree.selectedItem, "Nothing should be selected beforehand");
+
+ tree.once("select", pass);
+ let node = tree.root.children.firstChild.firstChild;
+ info("clicking on first top level item");
+ event = Promise.defer();
+ ok(!node.classList.contains("theme-selected"),
+ "Node should not have selected class before clicking");
+ click(node);
+ let [name, data, attachment] = yield event.promise;
+ ok(node.classList.contains("theme-selected"),
+ "Node has selected class after click");
+ is(data[0], "level1.2", "Correct tree path is emitted")
+ ok(attachment && attachment.foo, "Correct attachment is emitted")
+ is(attachment.foo, "bar", "Correct attachment value is emitted");
+
+ info("clicking second top level item with children to check if it expands");
+ let node2 = tree.root.children.firstChild.nextSibling.firstChild;
+ event = Promise.defer();
+ // node should not have selected class
+ ok(!node2.classList.contains("theme-selected"),
+ "New node should not have selected class before clicking");
+ ok(!node2.hasAttribute("expanded"), "New node is not expanded before clicking");
+ tree.once("select", pass);
+ click(node2);
+ [name, data, attachment] = yield event.promise;
+ ok(node2.classList.contains("theme-selected"),
+ "New node has selected class after clicking");
+ is(data[0], "level1", "Correct tree path is emitted for new node")
+ ok(!attachment, "null attachment should be emitted for new node")
+ ok(node2.hasAttribute("expanded"), "New node expanded after click");
+
+ ok(!node.classList.contains("theme-selected"),
+ "Old node should not have selected class after the click on new node");
+
+
+ // clicking again should just collapse
+ // this will not emit "select" event
+ event = Promise.defer();
+ node2.addEventListener("click", function onClick() {
+ node2.removeEventListener("click", onClick);
+ executeSoon(() => event.resolve(null));
+ });
+ click(node2);
+ yield event.promise;
+ ok(!node2.hasAttribute("expanded"), "New node collapsed after click again");
+}
diff --git a/toolkit/devtools/shared/test/doc_options-view.xul b/toolkit/devtools/shared/test/doc_options-view.xul
new file mode 100644
index 000000000..78d9956e9
--- /dev/null
+++ b/toolkit/devtools/shared/test/doc_options-view.xul
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <popupset id="options-popupset">
+ <menupopup id="options-menupopup" position="before_end">
+ <menuitem id="option-autoprettyprint"
+ type="checkbox"
+ data-pref="auto-pretty-print"
+ label="pretty print"/>
+ <menuitem id="option-autoblackbox"
+ type="checkbox"
+ data-pref="auto-black-box"
+ label="black box"/>
+ </menupopup>
+ </popupset>
+ <button id="options-button"
+ popup="options-menupopup"/>
+</window>
diff --git a/toolkit/devtools/shared/test/head.js b/toolkit/devtools/shared/test/head.js
new file mode 100644
index 000000000..121f75f69
--- /dev/null
+++ b/toolkit/devtools/shared/test/head.js
@@ -0,0 +1,241 @@
+/* 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/. */
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {TargetFactory, require} = devtools;
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+const {Hosts} = require("devtools/framework/toolbox-hosts");
+
+gDevTools.testing = true;
+SimpleTest.registerCleanupFunction(() => {
+ gDevTools.testing = false;
+});
+
+const TEST_URI_ROOT = "http://example.com/browser/browser/devtools/shared/test/";
+const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul";
+
+/**
+ * Open a new tab at a URL and call a callback on load
+ */
+function addTab(aURL, aCallback)
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ content.location = aURL;
+
+ let tab = gBrowser.selectedTab;
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ function onTabLoad() {
+ browser.removeEventListener("load", onTabLoad, true);
+ aCallback(browser, tab, browser.contentDocument);
+ }
+
+ browser.addEventListener("load", onTabLoad, true);
+}
+
+function promiseTab(aURL) {
+ return new Promise(resolve =>
+ addTab(aURL, resolve));
+}
+
+registerCleanupFunction(function tearDown() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+
+ console = undefined;
+});
+
+function catchFail(func) {
+ return function() {
+ try {
+ return func.apply(null, arguments);
+ }
+ catch (ex) {
+ ok(false, ex);
+ console.error(ex);
+ finish();
+ throw ex;
+ }
+ };
+}
+
+/**
+ * Polls a given function waiting for the given value.
+ *
+ * @param object aOptions
+ * Options object with the following properties:
+ * - validator
+ * A validator function that should return the expected value. This is
+ * called every few milliseconds to check if the result is the expected
+ * one. When the returned result is the expected one, then the |success|
+ * function is called and polling stops. If |validator| never returns
+ * the expected value, then polling timeouts after several tries and
+ * a failure is recorded - the given |failure| function is invoked.
+ * - success
+ * A function called when the validator function returns the expected
+ * value.
+ * - failure
+ * A function called if the validator function timeouts - fails to return
+ * the expected value in the given time.
+ * - name
+ * Name of test. This is used to generate the success and failure
+ * messages.
+ * - timeout
+ * Timeout for validator function, in milliseconds. Default is 5000 ms.
+ * - value
+ * The expected value. If this option is omitted then the |validator|
+ * function must return a trueish value.
+ * Each of the provided callback functions will receive two arguments:
+ * the |aOptions| object and the last value returned by |validator|.
+ */
+function waitForValue(aOptions)
+{
+ let start = Date.now();
+ let timeout = aOptions.timeout || 5000;
+ let lastValue;
+
+ function wait(validatorFn, successFn, failureFn)
+ {
+ if ((Date.now() - start) > timeout) {
+ // Log the failure.
+ ok(false, "Timed out while waiting for: " + aOptions.name);
+ let expected = "value" in aOptions ?
+ "'" + aOptions.value + "'" :
+ "a trueish value";
+ info("timeout info :: got '" + lastValue + "', expected " + expected);
+ failureFn(aOptions, lastValue);
+ return;
+ }
+
+ lastValue = validatorFn(aOptions, lastValue);
+ let successful = "value" in aOptions ?
+ lastValue == aOptions.value :
+ lastValue;
+ if (successful) {
+ ok(true, aOptions.name);
+ successFn(aOptions, lastValue);
+ }
+ else {
+ setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+ }
+ }
+
+ wait(aOptions.validator, aOptions.success, aOptions.failure);
+}
+
+function oneTimeObserve(name, callback) {
+ var func = function() {
+ Services.obs.removeObserver(func, name);
+ callback();
+ };
+ Services.obs.addObserver(func, name, false);
+}
+
+let createHost = Task.async(function*(type = "bottom", src = "data:text/html;charset=utf-8,") {
+ let host = new Hosts[type](gBrowser.selectedTab);
+ let iframe = yield host.create();
+
+ yield new Promise(resolve => {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ iframe.setAttribute("src", src);
+ domHelper.onceDOMReady(resolve);
+ });
+
+ return [host, iframe.contentWindow, iframe.contentDocument];
+});
+
+/**
+ * Load the Telemetry utils, then stub Telemetry.prototype.log in order to
+ * record everything that's logged in it.
+ * Store all recordings on Telemetry.telemetryInfo.
+ * @return {Telemetry}
+ */
+function loadTelemetryAndRecordLogs() {
+ info("Mock the Telemetry log function to record logged information");
+
+ let Telemetry = require("devtools/shared/telemetry");
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function(histogramId, value) {
+ if (!this.telemetryInfo) {
+ // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
+ return;
+ }
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+
+ this.telemetryInfo[histogramId].push(value);
+ }
+ };
+
+ return Telemetry;
+}
+
+/**
+ * Stop recording the Telemetry logs and put back the utils as it was before.
+ */
+function stopRecordingTelemetryLogs(Telemetry) {
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype.telemetryInfo;
+}
+
+/**
+ * Check the correctness of the data recorded in Telemetry after
+ * loadTelemetryAndRecordLogs was called.
+ */
+function checkTelemetryResults(Telemetry) {
+ let result = Telemetry.prototype.telemetryInfo;
+
+ for (let [histId, value] of Iterator(result)) {
+ if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+ ok(value.length === 1 && value[0] === true,
+ "Per user value " + histId + " has a single value of true");
+ } else if (histId.endsWith("OPENED_BOOLEAN")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element === true;
+ });
+
+ ok(okay, "All " + histId + " entries are === true");
+ } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
+ ok(value.length > 1, histId + " has more than one entry");
+
+ let okay = value.every(function(element) {
+ return element > 0;
+ });
+
+ ok(okay, "All " + histId + " entries have time > 0");
+ }
+ }
+}
+
+/**
+ * Open and close the toolbox in the current browser tab, several times, waiting
+ * some amount of time in between.
+ * @param {Number} nbOfTimes
+ * @param {Number} usageTime in milliseconds
+ * @param {String} toolId
+ */
+function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
+ for (let i = 0; i < nbOfTimes; i ++) {
+ info("Opening toolbox " + (i + 1));
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.showToolbox(target, toolId)
+
+ // We use a timeout to check the toolbox's active time
+ yield new Promise(resolve => setTimeout(resolve, usageTime));
+
+ info("Closing toolbox " + (i + 1));
+ yield gDevTools.closeToolbox(target);
+ }
+}
diff --git a/toolkit/devtools/shared/test/leakhunt.js b/toolkit/devtools/shared/test/leakhunt.js
new file mode 100644
index 000000000..66d067a3c
--- /dev/null
+++ b/toolkit/devtools/shared/test/leakhunt.js
@@ -0,0 +1,170 @@
+/* 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/. */
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ * thing: thing,
+ * otherthing: otherthing
+ * });
+ */
+function leakHunt(root) {
+ var path = [];
+ var seen = [];
+
+ try {
+ var output = leakHunt.inner(root, path, seen);
+ output.forEach(function(line) {
+ dump(line + '\n');
+ });
+ }
+ catch (ex) {
+ dump(ex + '\n');
+ }
+}
+
+leakHunt.inner = function LH_inner(root, path, seen) {
+ var prefix = new Array(path.length).join(' ');
+
+ var reply = [];
+ function log(msg) {
+ reply.push(msg);
+ }
+
+ var direct
+ try {
+ direct = Object.keys(root);
+ }
+ catch (ex) {
+ log(prefix + ' Error enumerating: ' + ex);
+ return reply;
+ }
+
+ try {
+ var index = 0;
+ for (var data of root) {
+ var prop = '' + index;
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ index++;
+ }
+ }
+ catch (ex) { /* Ignore things that are not enumerable */ }
+
+ for (var prop in root) {
+ var data;
+ try {
+ data = root[prop];
+ }
+ catch (ex) {
+ log(prefix + ' ' + prop + ' = Error: ' + ex.toString().substring(0, 30));
+ continue;
+ }
+
+ leakHunt.digProperty(prop, data, path, seen, direct, log);
+ }
+
+ return reply;
+}
+
+leakHunt.hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
+
+leakHunt.noRecurse = [
+ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
+ /^Window$/, /^Document$/,
+ /^XULDocument$/, /^XULElement$/,
+ /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/, /^ChromeWindow$/
+];
+
+leakHunt.digProperty = function LH_digProperty(prop, data, path, seen, direct, log) {
+ var newPath = path.slice();
+ newPath.push(prop);
+ var prefix = new Array(newPath.length).join(' ');
+
+ var recurse = true;
+ var message = leakHunt.getType(data);
+
+ if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) {
+ return;
+ }
+
+ if (message === 'function' && direct.indexOf(prop) == -1) {
+ return;
+ }
+
+ if (message === 'string') {
+ var extra = data.length > 10 ? data.substring(0, 9) + '_' : data;
+ message += ' "' + extra.replace(/\n/g, "|") + '"';
+ recurse = false;
+ }
+ else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) {
+ message += ' (no recurse)'
+ recurse = false;
+ }
+ else if (seen.indexOf(data) !== -1) {
+ message += ' (already seen)';
+ recurse = false;
+ }
+
+ if (recurse) {
+ seen.push(data);
+ var lines = leakHunt.inner(data, newPath, seen);
+ if (lines.length == 0) {
+ if (message !== 'function') {
+ log(prefix + prop + ' = ' + message + ' { }');
+ }
+ }
+ else {
+ log(prefix + prop + ' = ' + message + ' {');
+ lines.forEach(function(line) {
+ log(line);
+ });
+ log(prefix + '}');
+ }
+ }
+ else {
+ log(prefix + prop + ' = ' + message);
+ }
+};
+
+leakHunt.matchesAnyPattern = function LH_matchesAnyPattern(str, patterns) {
+ var match = false;
+ patterns.forEach(function(pattern) {
+ if (str.match(pattern)) {
+ match = true;
+ }
+ });
+ return match;
+};
+
+leakHunt.getType = function LH_getType(data) {
+ if (data === null) {
+ return 'null';
+ }
+ if (data === undefined) {
+ return 'undefined';
+ }
+
+ var type = typeof data;
+ if (type === 'object' || type === 'Object') {
+ type = leakHunt.getCtorName(data);
+ }
+
+ return type;
+};
+
+leakHunt.getCtorName = function LH_getCtorName(aObj) {
+ try {
+ if (aObj.constructor && aObj.constructor.name) {
+ return aObj.constructor.name;
+ }
+ }
+ catch (ex) {
+ return 'UnknownObject';
+ }
+
+ // If that fails, use Objects toString which sometimes gives something
+ // better than 'Object', and at least defaults to Object if nothing better
+ return Object.prototype.toString.call(aObj).slice(8, -1);
+};
diff --git a/toolkit/devtools/shared/test/unit/test_VariablesView_getString_promise.js b/toolkit/devtools/shared/test/unit/test_VariablesView_getString_promise.js
new file mode 100644
index 000000000..3c42e3ffb
--- /dev/null
+++ b/toolkit/devtools/shared/test/unit/test_VariablesView_getString_promise.js
@@ -0,0 +1,75 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { VariablesView } = Cu.import("resource:///modules/devtools/VariablesView.jsm", {});
+
+const PENDING = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "pending"
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const FULFILLED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const REJECTED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "rejected",
+ "reason": 10
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+function run_test() {
+ equal(VariablesView.getString(PENDING, { concise: true }), "Promise");
+ equal(VariablesView.getString(PENDING), 'Promise {<state>: "pending"}');
+
+ equal(VariablesView.getString(FULFILLED, { concise: true }), "Promise");
+ equal(VariablesView.getString(FULFILLED), 'Promise {<state>: "fulfilled", <value>: 10}');
+
+ equal(VariablesView.getString(REJECTED, { concise: true }), "Promise");
+ equal(VariablesView.getString(REJECTED), 'Promise {<state>: "rejected", <reason>: 10}');
+}
diff --git a/toolkit/devtools/shared/test/unit/test_bezierCanvas.js b/toolkit/devtools/shared/test/unit/test_bezierCanvas.js
new file mode 100644
index 000000000..21ffad1f0
--- /dev/null
+++ b/toolkit/devtools/shared/test/unit/test_bezierCanvas.js
@@ -0,0 +1,113 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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";
+
+// Tests the BezierCanvas API in the CubicBezierWidget module
+
+const Cu = Components.utils;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let require = devtools.require;
+let {CubicBezier, BezierCanvas} = require("devtools/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ offsetsGetterReturnsData();
+ convertsOffsetsToCoordinates();
+ plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+ do_print("offsets getter returns an array of 2 offset objects");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ let offsets = b.offsets;
+
+ do_check_eq(offsets.length, 2);
+
+ do_check_true("top" in offsets[0]);
+ do_check_true("left" in offsets[0]);
+ do_check_true("top" in offsets[1]);
+ do_check_true("left" in offsets[1]);
+
+ do_check_eq(offsets[0].top, "300px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "100px");
+ do_check_eq(offsets[1].left, "200px");
+
+ do_print("offsets getter returns data according to current padding");
+
+ b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+ offsets = b.offsets;
+
+ do_check_eq(offsets[0].top, "400px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "0px");
+ do_check_eq(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+ do_print("Converts offsets to coordinates");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+
+ let coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "0px"
+ }});
+ do_check_eq(coordinates.length, 2);
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 1.5);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "300px"
+ }});
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 0);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "200px",
+ top: "100px"
+ }});
+ do_check_eq(coordinates[0], 1);
+ do_check_eq(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+ do_print("Plots the curve to the canvas");
+
+ let hasDrawnCurve = false;
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ b.ctx.bezierCurveTo = () => hasDrawnCurve = true;
+ b.plot();
+
+ do_check_true(hasDrawnCurve);
+}
+
+function getCubicBezier() {
+ return new CubicBezier([0,0,1,1]);
+}
+
+function getCanvasMock(w=200, h=400) {
+ return {
+ getContext: function() {
+ return {
+ scale: () => {},
+ translate: () => {},
+ clearRect: () => {},
+ beginPath: () => {},
+ closePath: () => {},
+ moveTo: () => {},
+ lineTo: () => {},
+ stroke: () => {},
+ arc: () => {},
+ fill: () => {},
+ bezierCurveTo: () => {}
+ };
+ },
+ width: w,
+ height: h
+ };
+}
diff --git a/toolkit/devtools/shared/test/unit/test_cubicBezier.js b/toolkit/devtools/shared/test/unit/test_cubicBezier.js
new file mode 100644
index 000000000..b8a6231b2
--- /dev/null
+++ b/toolkit/devtools/shared/test/unit/test_cubicBezier.js
@@ -0,0 +1,102 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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";
+
+// Tests the CubicBezier API in the CubicBezierWidget module
+
+const Cu = Components.utils;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let require = devtools.require;
+let {CubicBezier} = require("devtools/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ throwsWhenMissingCoordinates();
+ throwsWhenIncorrectCoordinates();
+ convertsStringCoordinates();
+ coordinatesToStringOutputsAString();
+ pointGettersReturnPointCoordinatesArrays();
+ toStringOutputsCubicBezierValue();
+}
+
+function throwsWhenMissingCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier();
+ }, "Throws an exception when coordinates are missing");
+}
+
+function throwsWhenIncorrectCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier([]);
+ }, "Throws an exception when coordinates are incorrect (empty array)");
+
+ do_check_throws(() => {
+ new CubicBezier([0,0]);
+ }, "Throws an exception when coordinates are incorrect (incomplete array)");
+
+ do_check_throws(() => {
+ new CubicBezier(["a", "b", "c", "d"]);
+ }, "Throws an exception when coordinates are incorrect (invalid type)");
+
+ do_check_throws(() => {
+ new CubicBezier([1.5, 0, 1.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+
+ do_check_throws(() => {
+ new CubicBezier([-0.5, 0, -0.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+}
+
+function convertsStringCoordinates() {
+ do_print("Converts string coordinates to numbers");
+ let c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+ do_check_eq(c.coordinates[0], 0);
+ do_check_eq(c.coordinates[1], 1);
+ do_check_eq(c.coordinates[2], .5);
+ do_check_eq(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+ do_print("coordinates.toString() outputs a string representation");
+
+ let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+ let string = c.coordinates.toString();
+ do_check_eq(string, "0,1,.5,-2");
+
+ c = new CubicBezier([1, 1, 1, 1]);
+ string = c.coordinates.toString();
+ do_check_eq(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+ do_print("Points getters return arrays of coordinates");
+
+ let c = new CubicBezier([0, .2, .5, 1]);
+ do_check_eq(c.P1[0], 0);
+ do_check_eq(c.P1[1], .2);
+ do_check_eq(c.P2[0], .5);
+ do_check_eq(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+ do_print("toString() outputs the cubic-bezier() value");
+
+ let c = new CubicBezier([0, 0, 1, 1]);
+ do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
+}
+
+function do_check_throws(cb, info) {
+ do_print(info);
+
+ let hasThrown = false;
+ try {
+ cb();
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ do_check_true(hasThrown);
+}
diff --git a/toolkit/devtools/shared/test/unit/test_undoStack.js b/toolkit/devtools/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..ffc0666f9
--- /dev/null
+++ b/toolkit/devtools/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+let loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource:///modules/devtools",
+ },
+ globals: {},
+});
+let require = Loader.Require(loader, { id: "undo-test" })
+
+let {UndoStack} = require("devtools/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test()
+{
+ let str = "";
+ let stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(function() {
+ str += ch;
+ }, function() {
+ str = str.slice(0, -1);
+ });
+ }
+
+ do_check_false(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ do_check_true(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ do_check_eq(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ do_check_eq(str, "abcd");
+ do_check_true(stack.canRedo());
+
+ stack.redo();
+ do_check_eq(str, "abcde")
+ do_check_false(stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ do_check_eq(str, "abcd");
+
+ add("q");
+ do_check_eq(str, "abcdq");
+ do_check_false(stack.canRedo());
+
+ stack.undo();
+ do_check_eq(str, "abcd");
+ stack.redo();
+ do_check_eq(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ do_check_eq(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ do_check_eq(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ do_check_eq(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ do_check_eq(str, "abc");
+}
diff --git a/toolkit/devtools/shared/test/unit/xpcshell.ini b/toolkit/devtools/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b7b9b8a1b
--- /dev/null
+++ b/toolkit/devtools/shared/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
+
+[test_bezierCanvas.js]
+[test_cubicBezier.js]
+[test_undoStack.js]
+[test_VariablesView_getString_promise.js] \ No newline at end of file