diff --git a/addon-sdk/source/test/test-clipboard.js b/addon-sdk/source/test/test-clipboard.js index fd1efcaeeb79..f7ffd05be8f3 100644 --- a/addon-sdk/source/test/test-clipboard.js +++ b/addon-sdk/source/test/test-clipboard.js @@ -7,12 +7,11 @@ require("sdk/clipboard"); const { Cc, Ci } = require("chrome"); -const imageTools = Cc["@mozilla.org/image/tools;1"]. - getService(Ci.imgITools); - -const io = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); +const imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools); +const io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].getService(Ci.nsIAppShellService); +const XHTML_NS = "http://www.w3.org/1999/xhtml"; const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" + "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" + "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" + @@ -24,58 +23,6 @@ const { platform } = require("sdk/system"); // For Windows, Mac and Linux, platform returns the following: winnt, darwin and linux. var isWindows = platform.toLowerCase().indexOf("win") == 0; -const canvasHTML = "data:text/html," + encodeURIComponent( - "\ - \ - \ - \ - " -); - -function comparePixelImages(imageA, imageB, callback) { - let tabs = require("sdk/tabs"); - - tabs.open({ - url: canvasHTML, - - onReady: function onReady(tab) { - let worker = tab.attach({ - contentScript: "new " + function() { - let canvas = document.querySelector("canvas"); - let context = canvas.getContext("2d"); - - self.port.on("draw-image", function(imageURI) { - let img = new Image(); - - img.onload = function() { - context.drawImage(this, 0, 0); - - let pixels = Array.join(context.getImageData(0, 0, 32, 32).data); - self.port.emit("image-pixels", pixels); - } - - img.src = imageURI; - }); - } - }); - - let compared = ""; - - worker.port.on("image-pixels", function (pixels) { - if (!compared) { - compared = pixels; - this.emit("draw-image", imageB); - } else { - tab.close(callback.bind(null, compared === pixels)) - } - }); - - worker.port.emit("draw-image", imageA); - } - }); -} - - // Test the typical use case, setting & getting with no flavors specified exports["test With No Flavor"] = function(assert) { var contents = "hello there"; @@ -160,20 +107,39 @@ exports["test Set Image"] = function(assert) { assert.equal(clip.currentFlavors[0], flavor, "flavor is set"); }; -exports["test Get Image"] = function(assert, done) { +exports["test Get Image"] = function* (assert) { var clip = require("sdk/clipboard"); clip.set(base64png, "image"); var contents = clip.get(); + const hiddenWindow = appShellService.hiddenDOMWindow; + const Image = hiddenWindow.Image; + const canvas = hiddenWindow.document.createElementNS(XHTML_NS, "canvas"); + let context = canvas.getContext("2d"); - comparePixelImages(base64png, contents, function (areEquals) { - assert.ok(areEquals, - "Image gets from clipboard equals to image sets to the clipboard"); + const imageURLToPixels = (imageURL) => { + return new Promise((resolve) => { + let img = new Image(); - done(); - }); -} + img.onload = function() { + context.drawImage(this, 0, 0); + + let pixels = Array.join(context.getImageData(0, 0, 32, 32).data); + resolve(pixels); + }; + + img.src = imageURL; + }); + }; + + let [base64pngPixels, clipboardPixels] = yield Promise.all([ + imageURLToPixels(base64png), imageURLToPixels(contents), + ]); + + assert.ok(base64pngPixels === clipboardPixels, + "Image gets from clipboard equals to image sets to the clipboard"); +}; exports["test Set Image Type Not Supported"] = function(assert) { var clip = require("sdk/clipboard"); diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index 207cf8d3fa2e..239a7b0a8f38 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -889,9 +889,6 @@ pref("layout.accessiblecaret.enabled", true); // by the spec in bug 921965. pref("layout.accessiblecaret.bar.enabled", true); -// Hide the caret in cursor mode after 3 seconds. -pref("layout.accessiblecaret.timeout_ms", 3000); - // Hide carets and text selection dialog during scrolling. pref("layout.accessiblecaret.always_show_when_scrolling", false); diff --git a/browser/base/content/test/plugins/browser_blocklist_content.js b/browser/base/content/test/plugins/browser_blocklist_content.js index bf4e159bcc85..92583f10f446 100644 --- a/browser/base/content/test/plugins/browser_blocklist_content.js +++ b/browser/base/content/test/plugins/browser_blocklist_content.js @@ -72,7 +72,7 @@ add_task(function* () { // Hack the planet! Load our blocklist shim, so we can mess with blocklist // return results in the content process. Active until we close our tab. let mm = gTestBrowser.messageManager; - info("test 3a: loading " + gChromeRoot + "blocklist_proxy.js" + "\n"); + info("test 3a: loading " + gChromeRoot + "blocklist_proxy.js\n"); mm.loadFrameScript(gChromeRoot + "blocklist_proxy.js", true); yield promiseTabLoadEvent(gBrowser.selectedTab, gTestRoot + "plugin_test.html"); diff --git a/browser/components/.eslintrc.js b/browser/components/.eslintrc.js new file mode 100644 index 000000000000..63dbf791fa0a --- /dev/null +++ b/browser/components/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + rules: { + // XXX Bug 1326071 - This should be reduced down - probably to 20 or to + // be removed & synced with the toolkit/.eslintrc.js value. + "complexity": ["error", {"max": 69}], + } +}; diff --git a/browser/components/customizableui/test/browser_1087303_button_preferences.js b/browser/components/customizableui/test/browser_1087303_button_preferences.js index b1fdb85b6de5..91b48e21f618 100644 --- a/browser/components/customizableui/test/browser_1087303_button_preferences.js +++ b/browser/components/customizableui/test/browser_1087303_button_preferences.js @@ -42,7 +42,7 @@ function waitForPageLoad(aTab) { function onTabLoad(event) { clearTimeout(timeoutId); aTab.linkedBrowser.removeEventListener("load", onTabLoad, true); - info("Tab event received: " + "load"); + info("Tab event received: load"); deferred.resolve(); } aTab.linkedBrowser.addEventListener("load", onTabLoad, true, true); diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index f4cc5c9d9744..868978cccff8 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -86,6 +86,8 @@ support-files = [browser_ext_sessions_restore.js] [browser_ext_sidebarAction.js] [browser_ext_sidebarAction_context.js] +[browser_ext_sidebarAction_tabs.js] +[browser_ext_sidebarAction_windows.js] [browser_ext_simple.js] [browser_ext_tab_runtimeConnect.js] [browser_ext_tabs_audio.js] diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js index 1e27fb082b38..5aea4d96823d 100644 --- a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js +++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js @@ -85,36 +85,6 @@ add_task(function* sidebar_two_sidebar_addons() { yield extension2.unload(); }); -add_task(function* sidebar_windows() { - let extension = ExtensionTestUtils.loadExtension(extData); - yield extension.startup(); - // Test sidebar is opened on install - yield extension.awaitMessage("sidebar"); - ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible in first window"); - // Check that the menuitem has our image styling. - let elements = document.getElementsByClassName("webextension-menuitem"); - is(elements.length, 1, "have one menuitem"); - let style = elements[0].getAttribute("style"); - ok(style.includes("webextension-menuitem-image"), "this menu has style"); - - let secondSidebar = extension.awaitMessage("sidebar"); - - // SidebarUI relies on window.opener being set, which is normal behavior when - // using menu or key commands to open a new browser window. - let win = yield BrowserTestUtils.openNewBrowserWindow({opener: window}); - - yield secondSidebar; - ok(!win.document.getElementById("sidebar-box").hidden, "sidebar box is visible in second window"); - // Check that the menuitem has our image styling. - elements = win.document.getElementsByClassName("webextension-menuitem"); - is(elements.length, 1, "have one menuitem"); - style = elements[0].getAttribute("style"); - ok(style.includes("webextension-menuitem-image"), "this menu has style"); - - yield extension.unload(); - yield BrowserTestUtils.closeWindow(win); -}); - add_task(function* sidebar_empty_panel() { let extension = ExtensionTestUtils.loadExtension(extData); yield extension.startup(); @@ -126,49 +96,6 @@ add_task(function* sidebar_empty_panel() { yield extension.unload(); }); -add_task(function* sidebar_tab_query_bug_1340739() { - let data = { - manifest: { - "permissions": [ - "tabs", - ], - "sidebar_action": { - "default_panel": "sidebar.html", - }, - }, - useAddonManager: "temporary", - files: { - "sidebar.html": ` - - - - - - - A Test Sidebar - - `, - "sidebar.js": function() { - Promise.all([ - browser.tabs.query({}).then((tabs) => { - browser.test.assertEq(1, tabs.length, "got tab without currentWindow"); - }), - browser.tabs.query({currentWindow: true}).then((tabs) => { - browser.test.assertEq(1, tabs.length, "got tab with currentWindow"); - }), - ]).then(() => { - browser.test.sendMessage("sidebar"); - }); - }, - }, - }; - - let extension = ExtensionTestUtils.loadExtension(data); - yield extension.startup(); - yield extension.awaitMessage("sidebar"); - yield extension.unload(); -}); - add_task(function* cleanup() { // This is set on initial sidebar install. Services.prefs.clearUserPref("extensions.sidebar-button.shown"); diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js new file mode 100644 index 000000000000..e8e6c04e2a24 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js @@ -0,0 +1,52 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(function* sidebar_tab_query_bug_1340739() { + let data = { + manifest: { + "permissions": [ + "tabs", + ], + "sidebar_action": { + "default_panel": "sidebar.html", + }, + }, + useAddonManager: "temporary", + files: { + "sidebar.html": ` + + + + + + + A Test Sidebar + + `, + "sidebar.js": function() { + Promise.all([ + browser.tabs.query({}).then((tabs) => { + browser.test.assertEq(1, tabs.length, "got tab without currentWindow"); + }), + browser.tabs.query({currentWindow: true}).then((tabs) => { + browser.test.assertEq(1, tabs.length, "got tab with currentWindow"); + }), + ]).then(() => { + browser.test.sendMessage("sidebar"); + }); + }, + }, + }; + + let extension = ExtensionTestUtils.loadExtension(data); + yield extension.startup(); + yield extension.awaitMessage("sidebar"); + yield extension.unload(); + + // Move toolbar button back to customization. + CustomizableUI.removeWidgetFromArea("sidebar-button", CustomizableUI.AREA_NAVBAR); + ok(!document.getElementById("sidebar-button"), "sidebar button is not in UI"); + // This is set on initial sidebar install. + Services.prefs.clearUserPref("extensions.sidebar-button.shown"); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js new file mode 100644 index 000000000000..ed9b88d651d5 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js @@ -0,0 +1,67 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +let extData = { + manifest: { + "sidebar_action": { + "default_panel": "sidebar.html", + }, + }, + useAddonManager: "temporary", + + files: { + "sidebar.html": ` + + + + + + + A Test Sidebar + + `, + + "sidebar.js": function() { + window.onload = () => { + browser.test.sendMessage("sidebar"); + }; + }, + }, +}; + +add_task(function* sidebar_windows() { + let extension = ExtensionTestUtils.loadExtension(extData); + yield extension.startup(); + // Test sidebar is opened on install + yield extension.awaitMessage("sidebar"); + ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible in first window"); + // Check that the menuitem has our image styling. + let elements = document.getElementsByClassName("webextension-menuitem"); + is(elements.length, 1, "have one menuitem"); + let style = elements[0].getAttribute("style"); + ok(style.includes("webextension-menuitem-image"), "this menu has style"); + + let secondSidebar = extension.awaitMessage("sidebar"); + + // SidebarUI relies on window.opener being set, which is normal behavior when + // using menu or key commands to open a new browser window. + let win = yield BrowserTestUtils.openNewBrowserWindow({opener: window}); + + yield secondSidebar; + ok(!win.document.getElementById("sidebar-box").hidden, "sidebar box is visible in second window"); + // Check that the menuitem has our image styling. + elements = win.document.getElementsByClassName("webextension-menuitem"); + is(elements.length, 1, "have one menuitem"); + style = elements[0].getAttribute("style"); + ok(style.includes("webextension-menuitem-image"), "this menu has style"); + + yield extension.unload(); + yield BrowserTestUtils.closeWindow(win); + + // Move toolbar button back to customization. + CustomizableUI.removeWidgetFromArea("sidebar-button", CustomizableUI.AREA_NAVBAR); + ok(!document.getElementById("sidebar-button"), "sidebar button is not in UI"); + // This is set on initial sidebar install. + Services.prefs.clearUserPref("extensions.sidebar-button.shown"); +}); diff --git a/browser/components/migration/.eslintrc.js b/browser/components/migration/.eslintrc.js index 570cd076b7ce..bd6359f02ed4 100644 --- a/browser/components/migration/.eslintrc.js +++ b/browser/components/migration/.eslintrc.js @@ -19,7 +19,7 @@ module.exports = { // eslint-disable-line no-undef "comma-dangle": "off", "comma-spacing": ["warn", {"before": false, "after": true}], "comma-style": ["warn", "last"], - // "complexity": "warn", + "complexity": ["error", {"max": 21}], "consistent-return": "error", //"curly": "error", "dot-notation": "error", @@ -79,4 +79,3 @@ module.exports = { // eslint-disable-line no-undef "yoda": "error" } }; - diff --git a/browser/components/migration/ESEDBReader.jsm b/browser/components/migration/ESEDBReader.jsm index ad842356b338..92d6ba310467 100644 --- a/browser/components/migration/ESEDBReader.jsm +++ b/browser/components/migration/ESEDBReader.jsm @@ -432,7 +432,7 @@ ESEDB.prototype = { // Deal with null values: buffer = null; } else { - Cu.reportError("Unexpected JET error: " + err + ";" + " retrieving value for column " + column.name); + Cu.reportError("Unexpected JET error: " + err + "; retrieving value for column " + column.name); throw new Error(convertESEError(err)); } } diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js index 38fb91f72106..6dbc60381682 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js @@ -30,7 +30,7 @@ add_task(function* test() { } else { page_with_title = test_title + " - " + app_name; page_without_title = app_name; - about_pb_title = "Open a private window?" + " - " + app_name; + about_pb_title = "Open a private window? - " + app_name; pb_page_with_title = test_title + " - " + app_name + " (Private Browsing)"; pb_page_without_title = app_name + " (Private Browsing)"; pb_about_pb_title = "Private Browsing - " + app_name + " (Private Browsing)"; diff --git a/browser/extensions/formautofill/.eslintrc.js b/browser/extensions/formautofill/.eslintrc.js index 0544208c163e..6224ec2adb04 100644 --- a/browser/extensions/formautofill/.eslintrc.js +++ b/browser/extensions/formautofill/.eslintrc.js @@ -109,7 +109,7 @@ module.exports = { // eslint-disable-line no-undef "comma-dangle": ["error", "always-multiline"], // Warn about cyclomatic complexity in functions. - "complexity": "warn", + "complexity": ["error", {"max": 20}], // Don't warn for inconsistent naming when capturing this (not so important // with auto-binding fat arrow functions). diff --git a/browser/extensions/pdfjs/test/browser_pdfjs_zoom.js b/browser/extensions/pdfjs/test/browser_pdfjs_zoom.js index fadcd4c7e352..7720d9a8d2c4 100644 --- a/browser/extensions/pdfjs/test/browser_pdfjs_zoom.js +++ b/browser/extensions/pdfjs/test/browser_pdfjs_zoom.js @@ -72,7 +72,7 @@ add_task(function* test() { yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (newTabBrowser) { - yield waitForPdfJS(newTabBrowser, TESTROOT + "file_pdfjs_test.pdf" + "#zoom=100"); + yield waitForPdfJS(newTabBrowser, TESTROOT + "file_pdfjs_test.pdf#zoom=100"); yield ContentTask.spawn(newTabBrowser, TESTS, function* (contentTESTS) { let document = content.document; diff --git a/devtools/client/inspector/rules/test/browser_rules_authored.js b/devtools/client/inspector/rules/test/browser_rules_authored.js index cb0dd11866e5..3471d782e82e 100644 --- a/devtools/client/inspector/rules/test/browser_rules_authored.js +++ b/devtools/client/inspector/rules/test/browser_rules_authored.js @@ -26,7 +26,7 @@ add_task(function* () { " color: orang;" + // Override. " background-color: blue;" + - " background-color: #f0c;" + + " background-color: #f06;" + "} "); let elementStyle = view._elementStyle; diff --git a/devtools/client/inspector/rules/test/browser_rules_authored_color.js b/devtools/client/inspector/rules/test/browser_rules_authored_color.js index 4c5cab206744..7507299a7cbc 100644 --- a/devtools/client/inspector/rules/test/browser_rules_authored_color.js +++ b/devtools/client/inspector/rules/test/browser_rules_authored_color.js @@ -14,10 +14,10 @@ * {String} result: expected value of the color property after edition. */ const colors = [ - {name: "hex", id: "test1", color: "#f0c", result: "#0f0"}, + {name: "hex", id: "test1", color: "#f06", result: "#0f0"}, {name: "rgb", id: "test2", color: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}, // Test case preservation. - {name: "hex", id: "test3", color: "#F0C", result: "#0F0"}, + {name: "hex", id: "test3", color: "#F06", result: "#0F0"}, ]; add_task(function* () { diff --git a/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html index 8fce0458406e..9964bf506969 100644 --- a/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html +++ b/devtools/client/inspector/rules/test/doc_keyframeLineNumbers.html @@ -23,7 +23,7 @@ span { background: #ffffff; } to { - background: #f0c; + background: #f06; } } diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js index afc92072c705..fe87d623c2ad 100644 --- a/devtools/client/shared/test/browser_outputparser.js +++ b/devtools/client/shared/test/browser_outputparser.js @@ -152,7 +152,7 @@ function testParseCssProperty(doc, parser) { " 100%)"]), // Note the lack of a space before the color here. - makeColorTest("border", "1px dotted#f0c", ["1px dotted ", {name: "#f0c"}]), + makeColorTest("border", "1px dotted#f06", ["1px dotted ", {name: "#f06"}]), ]; let target = doc.querySelector("div"); diff --git a/devtools/client/shared/test/unit/test_cssColor-01.js b/devtools/client/shared/test/unit/test_cssColor-01.js index 13b9b5fa0b6b..311be4ebd616 100644 --- a/devtools/client/shared/test/unit/test_cssColor-01.js +++ b/devtools/client/shared/test/unit/test_cssColor-01.js @@ -24,8 +24,8 @@ const CLASSIFY_TESTS = [ { input: "hsl(5, 5%, 5%)", output: "hsl" }, { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" }, { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" }, - { input: "#f0c", output: "hex" }, - { input: "#f0c0", output: "hex" }, + { input: "#f06", output: "hex" }, + { input: "#f060", output: "hex" }, { input: "#fe01cb", output: "hex" }, { input: "#fe01cb80", output: "hex" }, { input: "#FE01CB", output: "hex" }, diff --git a/devtools/client/shared/test/unit/test_rewriteDeclarations.js b/devtools/client/shared/test/unit/test_rewriteDeclarations.js index 8fe7772fc690..b49e63501d75 100644 --- a/devtools/client/shared/test/unit/test_rewriteDeclarations.js +++ b/devtools/client/shared/test/unit/test_rewriteDeclarations.js @@ -457,7 +457,7 @@ const TEST_DATA = [ { desc: "delete disabled property", - input: "\n a:b;\n /* color:#f0c; */\n e:f;", + input: "\n a:b;\n /* color:#f06; */\n e:f;", instruction: {type: "remove", name: "color", index: 1}, expected: "\n a:b;\n e:f;", }, @@ -469,7 +469,7 @@ const TEST_DATA = [ }, { desc: "delete disabled property leaving other disabled property", - input: "\n a:b;\n /* color:#f0c; background-color: seagreen; */\n e:f;", + input: "\n a:b;\n /* color:#f06; background-color: seagreen; */\n e:f;", instruction: {type: "remove", name: "color", index: 1}, expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;", }, diff --git a/devtools/docs/SUMMARY.md b/devtools/docs/SUMMARY.md index 00ed204c64b3..f6b8c2e29a9c 100644 --- a/devtools/docs/SUMMARY.md +++ b/devtools/docs/SUMMARY.md @@ -2,7 +2,9 @@ # Summary * [Tool Architectures](tools.md) - * [Inspector](inspector-panel.md) + * [Inspector](inspector.md) + * [Panel Architecture](inspector-panel.md) + * [Highlighters](highlighters.md) * [Memory](memory-panel.md) * [Debugger](debugger-panel.md) * [Responsive Design Mode](responsive-design-mode.md) diff --git a/devtools/docs/highlighters.md b/devtools/docs/highlighters.md new file mode 100644 index 000000000000..d9854b516d53 --- /dev/null +++ b/devtools/docs/highlighters.md @@ -0,0 +1,201 @@ +# Highlighters + +This article provides technical documentation about DevTools highlighters. + +By highlighter, we mean anything that DevTools displays on top of the content page, in order to highlight an element, a set of elements or shapes to users. + +The most obvious form of highlighter is the box-model highlighter, whose job is to display the 4 box-model regions on top of a given element in the content page, as illustrated in the following screen capture: + +![Box-model highlighter](./img/box-model-highlighter-screenshot.png) + +But there can be a wide variety of highlighters. In particular, highlighters are a pretty good way to give detailed information about: + +* the exact form of a css shape, +* how a css transform applied to an element, +* where are the color stops of a css gradient, +* which are all the elements that match a given selector, +* ... + +## Using highlighters + +Highlighters run on the debuggee side, not on the toolbox side. This is so that it's possible to highlight elements on a remote device for instance. This means you need to go through the [Remote Debugging Protocol](protocol.md) to use a highlighter. + +### The highlighter utils + +The easiest way to access the highlighters from toolbox-side DevTools code is by using the highlighter utils, which is conveniently available on the toolbox object. Here is how you can access the utils: + +```js +let hUtils = toolbox.highlighterUtils; +``` + +Since the box-model highlighter is the most used type of highlighter (for instance it's displayed when you move your mouse over nodes in the inspector), the utils provides a set of methods to interact with it: + +| Method | Description | +|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `startPicker()` | Starts the node picker mode which will highlight every node you hover over in the page, and will change the current node selection in the inspector on click. “picker-node-hovered” and “picker-node-picked” events are sent. | +| `stopPicker()` | Stops the node picker mode. | +| `highlightNodeFront(nodeFront)` | Display the box-model highlighter on a given node. NodeFront objects are what the WalkerActor return. | +| `highlightDomValueGrip(valueGrip)` | Display the box-model highlighter on a given node, represented by a debugger object value grip. | +| `unhighlight()` | Hide the box-model highlighter. | + +But the box-model highlighter isn't the only type of highlighter, so the highlighter utils provides the following method: + +| Method | Description | +|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `getHighlighterByType(typeName)` | Instantiate a new highlighter, given its type (as a String). At the time of writing, the available types of highlighters are: `BoxModelHighlighter`, `CssTransformHighlighter`, `SelectorHighlighter` and `RectHighlighter`. This returns a promise that resolves to the new instance of [protocol.js](https://wiki.mozilla.org/DevTools/protocol.js) actor. | + +### The highlighter API + +When getting a highlighter via `toolbox.highlighterUtils.getHighlighterByType(typeName)`, the right type of highlighter will be instantiated on the server-side and will be wrapped into a `CustomHighlighterActor` and that's what will be returned to the caller. This means that all types of highlighters share the same following API: + +| Method | Description | +|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `show(NodeActor node[, Object options])` | Highlighters are hidden by default. Calling this method is what makes them visible. The first, mandatory, parameter should be a NodeActor. NodeActors are what the WalkerActor return. It's easy to get a NodeActor for an existing DOM node. For example `toolbox.walker.querySelector(toolbox.walker.rootNode, "css selector")` resolves to a NodeFront (the client-side version of the NodeActor) which can be used as the first parameter. The second, optional, parameter depends on the type of highlighter being used. | +| `hide()` | Hides the highlighter. | +| `finalize()` | Destroys the highlighter. | + +## Creating new highlighters + +Before digging into how one goes about adding a new type of highlighter to the DevTools, it is worth understanding how are highlighters displayed in the page. + +### Inserting content in the page + +Highlighters use web technology themselves to display the required information on screen. For instance, the box-model highlighter uses SVG to draw the margin, border, padding and content regions over the highlighted node. + +This means the highlighter content needs to be inserted in the page, but in a non-intrusive way. Indeed, the DevTools should never alter the page unless the alteration was done by the user (like changing the DOM using the inspector or a CSS rule via the style-editor for example). So simply appending the highlighter's markup in the content document is not an option. + +Furthermore, highlighters not only need to work with Firefox Desktop, but they should work just as well on Firefox OS, Firefox for Android, and more generally anything that runs the Gecko rendering engine. Therefore appending the highlighter's markup to the browser chrome XUL structure isn't an option either. + +To this end, DevTools highlighters make use of a (chrome-only) API: + +``` + /** + * Chrome document anonymous content management. + * This is a Chrome-only API that allows inserting fixed positioned anonymous + * content on top of the current page displayed in the document. + * The supplied content is cloned and inserted into the document's CanvasFrame. + * Note that this only works for HTML documents. + */ + partial interface Document { + /** + * Deep-clones the provided element and inserts it into the CanvasFrame. + * Returns an AnonymousContent instance that can be used to manipulate the + * inserted element. + */ + [ChromeOnly, NewObject, Throws] + AnonymousContent insertAnonymousContent(Element aElement); + + /** + * Removes the element inserted into the CanvasFrame given an AnonymousContent + * instance. + */ + [ChromeOnly, Throws] + void removeAnonymousContent(AnonymousContent aContent); + }; +``` + +Using this API, it is possible for chrome-privileged JS to insert arbitrary DOM elements on top of the content page. + +Technically, the DOM element is inserted into the `CanvasFrame` of the document. The `CanvasFrame` is part of the rendered frame tree and the DOM element is part of the native anonymous elements of the `CanvasFrame`. + +Consider the following simple example: + +```js + let el = document.createElement("div"); + el.textContent = "My test element"; + let insertedEl = document.insertAnonymousContent(el); +``` + +In this example, the test DIV will be inserted in the page, and will be displayed on top of everything else, in a way that doesn't impact the current layout. + +### The AnonymousContent API + +In the previous example, the returned `insertedEl` object isn't a DOM node, and it certainly is not `el`. It is a new object, whose type is `AnonymousContent` ([see the WebIDL here](http://mxr.mozilla.org/mozilla-central/source/dom/webidl/AnonymousContent.webidl?force=1)). + +Because of the way content is inserted into the page, it isn't wanted to give consumers a direct reference to the inserted DOM node. This is why `document.insertAnonymousContent(el)` actually **clones** `el` and returns a new object whose API lets consumers make changes to the inserted element in a way that never gives back a reference to the inserted DOM node. + +### CanvasFrameAnonymousContentHelper + +In order to help with the API described in the previous section, the `CanvasFrameAnonymousContentHelper` class was introduced. + +Its goal is to provide a simple way for highlighters to insert their content into the page and modify it dynamically later. One of its goal is also to re-insert the highlighters' content on page navigation. Indeed, the frame tree is destroyed when the page is navigated away from since it represents the document element. + +Using this helper is quite simple: + +```js +let helper = new CanvasFrameAnonymousContentHelper(tabActor, this.buildMarkup.bind(this)); +``` + +It only requires a `tabActor`, which highlighters get when they are instantiated, and a callback function that will be used to create and insert the content the first time the highlighter is shown, and every time there's a page navigation. + +The returned object provides the following API: + +| Method | Description | +|-------------------------------------------|------------------------------------------------------------| +| `getTextContentForElement(id)` | Get the textContent of an element given its ID. | +| `setTextContentForElement(id, text)` | Set the textContent of an element given its ID. | +| `setAttributeForElement(id, name, value)` | Set an attribute value of an element given its ID. | +| `getAttributeForElement(id, name)` | Get an attribute value of an element given its ID. | +| `removeAttributeForElement(id, name)` | Remove an attribute of an element given its ID. | +| `content` | This property returns the wrapped AnonymousContent object. | +| `destroy()` | Destroy the helper instance. | + + ### Creating a new highlighter class + +A good way to get started is by taking a look at [existing highlighters here](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/highlighter.js). + +Here is some boilerplate code for a new highlighter class: + +```js + function MyNewHighlighter(tabActor) { + this.doc = tabActor.window.document; + this.markup = new CanvasFrameAnonymousContentHelper(tabActor, this._buildMarkup.bind(this)); + } + + MyNewHighlighter.prototype = { + destroy: function() { + this.doc = null; + this.markup.destroy(); + }, + + _buildMarkup: function() { + let container = this.doc.createElement("div"); + container.innerHTML = '