Bug 1321238 - Add unit tests for the Grid List and Display setting components. r=jdescottes
authorGabriel Luong <gabriel.luong@gmail.com>
Mon, 29 May 2017 15:54:35 -0400
changeset 586207 2cba4fc3c88a70702bddfaffb10863e10c36d407
parent 586206 fe64a3b770fd421ee62896bce58369275555281b
child 586208 acca723541c25889cad344f19087983c9d31fa1b
push id61331
push userbmo:cpearce@mozilla.com
push dateMon, 29 May 2017 23:54:26 +0000
reviewersjdescottes
bugs1321238
milestone55.0a1
Bug 1321238 - Add unit tests for the Grid List and Display setting components. r=jdescottes
devtools/client/inspector/boxmodel/box-model.js
devtools/client/inspector/grids/components/GridDisplaySettings.js
devtools/client/inspector/grids/components/GridList.js
devtools/client/inspector/grids/grid-inspector.js
devtools/client/inspector/grids/moz.build
devtools/client/inspector/grids/reducers/grids.js
devtools/client/inspector/grids/test/.eslintrc.js
devtools/client/inspector/grids/test/browser.ini
devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js
devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js
devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js
devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js
devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js
devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js
devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js
devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js
devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js
devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-single-grid.js
devtools/client/inspector/grids/test/head.js
devtools/client/inspector/rules/test/head.js
devtools/client/inspector/shared/highlighters-overlay.js
devtools/client/inspector/test/head.js
--- a/devtools/client/inspector/boxmodel/box-model.js
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -122,41 +122,45 @@ BoxModel.prototype = {
    */
   updateBoxModel(reason) {
     this._updateReasons = this._updateReasons || [];
     if (reason) {
       this._updateReasons.push(reason);
     }
 
     let lastRequest = Task.spawn((function* () {
-      if (!(this.isPanelVisible() &&
-          this.inspector.selection.isConnected() &&
-          this.inspector.selection.isElementNode())) {
+      if (!this.inspector ||
+          !this.isPanelVisible() ||
+          !this.inspector.selection.isConnected() ||
+          !this.inspector.selection.isElementNode()) {
         return null;
       }
 
       let node = this.inspector.selection.nodeFront;
+
       let layout = yield this.inspector.pageStyle.getLayout(node, {
         autoMargins: true,
       });
+
       let styleEntries = yield this.inspector.pageStyle.getApplied(node, {
         // We don't need styles applied to pseudo elements of the current node.
         skipPseudo: true
       });
       this.elementRules = styleEntries.map(e => e.rule);
 
       // Update the layout properties with whether or not the element's position is
       // editable with the geometry editor.
       let isPositionEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+
       layout = Object.assign({}, layout, {
         isPositionEditable,
       });
 
-      const actorCanGetOffSetParent
-        = yield this.inspector.target.actorHasMethod("domwalker", "getOffsetParent");
+      const actorCanGetOffSetParent =
+        yield this.inspector.target.actorHasMethod("domwalker", "getOffsetParent");
 
       if (actorCanGetOffSetParent) {
         // Update the redux store with the latest offset parent DOM node
         let offsetParent = yield this.inspector.walker.getOffsetParent(node);
         this.store.dispatch(updateOffsetParent(offsetParent));
       }
 
       // Update the redux store with the latest layout properties and update the box
--- a/devtools/client/inspector/grids/components/GridDisplaySettings.js
+++ b/devtools/client/inspector/grids/components/GridDisplaySettings.js
@@ -58,32 +58,34 @@ module.exports = createClass({
         dom.li(
           {
             className: "grid-settings-item",
           },
           dom.label(
             {},
             dom.input(
               {
+                id: "grid-setting-extend-grid-lines",
                 type: "checkbox",
                 checked: highlighterSettings.showInfiniteLines,
                 onChange: this.onShowInfiniteLinesCheckboxClick,
               }
             ),
             getStr("layout.extendGridLinesInfinitely")
           )
         ),
         dom.li(
           {
             className: "grid-settings-item",
           },
           dom.label(
             {},
             dom.input(
               {
+                id: "grid-setting-show-grid-line-numbers",
                 type: "checkbox",
                 checked: highlighterSettings.showGridLineNumbers,
                 onChange: this.onShowGridLineNumbersCheckboxClick,
               }
             ),
             getStr("layout.displayNumbersOnLines")
           )
         )
--- a/devtools/client/inspector/grids/components/GridList.js
+++ b/devtools/client/inspector/grids/components/GridList.js
@@ -43,17 +43,19 @@ module.exports = createClass({
       {
         className: "grid-container",
       },
       dom.span(
         {},
         getStr("layout.overlayGrid")
       ),
       dom.ul(
-        {},
+        {
+          id: "grid-list",
+        },
         grids.map(grid => GridItem({
           key: grid.id,
           getSwatchColorPickerTooltip,
           grid,
           setSelectedNode,
           onHideBoxModelHighlighter,
           onSetGridOverlayColor,
           onShowBoxModelHighlighterForNode,
--- a/devtools/client/inspector/grids/grid-inspector.js
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -248,23 +248,37 @@ GridInspector.prototype = {
   updateGridPanel: Task.async(function* (gridFronts) {
     // Stop refreshing if the inspector or store is already destroyed.
     if (!this.inspector || !this.store) {
       return;
     }
 
     // Get all the GridFront from the server if no gridFronts were provided.
     if (!gridFronts) {
-      gridFronts = yield this.layoutInspector.getAllGrids(this.walker.rootNode);
+      try {
+        gridFronts = yield this.layoutInspector.getAllGrids(this.walker.rootNode);
+      } catch (e) {
+        // This call might fail if called asynchrously after the toolbox is finished
+        // closing.
+        return;
+      }
     }
 
     let grids = [];
     for (let i = 0; i < gridFronts.length; i++) {
       let grid = gridFronts[i];
-      let nodeFront = yield this.walker.getNodeFromActor(grid.actorID, ["containerEl"]);
+
+      let nodeFront;
+      try {
+        nodeFront = yield this.walker.getNodeFromActor(grid.actorID, ["containerEl"]);
+      } catch (e) {
+        // This call might fail if called asynchrously after the toolbox is finished
+        // closing.
+        return;
+      }
 
       let fallbackColor = GRID_COLORS[i % GRID_COLORS.length];
       let color = this.getInitialGridColor(nodeFront, fallbackColor);
 
       grids.push({
         id: i,
         color,
         gridFragments: grid.gridFragments,
@@ -295,17 +309,17 @@ GridInspector.prototype = {
    * @param  {Event} event
    *         Event that was triggered.
    * @param  {NodeFront} nodeFront
    *         The NodeFront of the grid container element for which the grid highlighter
    *         is shown for.
    * @param  {Object} options
    *         The highlighter options used for the highlighter being shown/hidden.
    */
-  onHighlighterChange(event, nodeFront, options) {
+  onHighlighterChange(event, nodeFront, options = {}) {
     let highlighted = event === "grid-highlighter-shown";
     let { color } = options;
 
     // Only tell the store that the highlighter changed if it did change.
     // If we're still highlighting the same node, with the same color, no need to force
     // a refresh.
     if (this.lastHighlighterState !== highlighted ||
         this.lastHighlighterNode !== nodeFront) {
--- a/devtools/client/inspector/grids/moz.build
+++ b/devtools/client/inspector/grids/moz.build
@@ -10,8 +10,10 @@ DIRS += [
     'reducers',
     'utils',
 ]
 
 DevToolsModules(
     'grid-inspector.js',
     'types.js',
 )
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/grids/reducers/grids.js
+++ b/devtools/client/inspector/grids/reducers/grids.js
@@ -12,17 +12,17 @@ const {
 
 const INITIAL_GRIDS = [];
 
 let reducers = {
 
   [UPDATE_GRID_COLOR](grids, { nodeFront, color }) {
     let newGrids = grids.map(g => {
       if (g.nodeFront == nodeFront) {
-        g.color = color;
+        g = Object.assign({}, g, { color });
       }
 
       return g;
     });
 
     return newGrids;
   },
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../../.eslintrc.mochitests.js",
+  "globals": {
+    "waitUntilState": true
+  }
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  head.js
+  !/devtools/client/commandline/test/helpers.js
+  !/devtools/client/framework/test/shared-head.js
+  !/devtools/client/inspector/test/head.js
+  !/devtools/client/inspector/test/shared-head.js
+  !/devtools/client/shared/test/test-actor.js
+  !/devtools/client/shared/test/test-actor-registry.js
+  !/devtools/client/framework/test/shared-redux-head.js
+
+[browser_grids_display-setting-extend-grid-lines.js]
+[browser_grids_display-setting-show-grid-line-numbers.js]
+[browser_grids_grid-list-color-picker-on-ESC.js]
+[browser_grids_grid-list-color-picker-on-RETURN.js]
+[browser_grids_grid-list-element-rep.js]
+[browser_grids_grid-list-no-grids.js]
+[browser_grids_grid-list-on-mutation-element-added.js]
+[browser_grids_grid-list-on-mutation-element-removed.js]
+[browser_grids_grid-list-toggle-multiple-grids.js]
+[browser_grids_grid-list-toggle-single-grid.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_display-setting-extend-grid-lines.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 'Extend grid lines infinitely' grid highlighter setting will update
+// the redux store and pref setting.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { store } = inspector;
+
+  yield selectNode("#grid", inspector);
+  let checkbox = doc.getElementById("grid-setting-extend-grid-lines");
+
+  ok(!Services.prefs.getBoolPref(SHOW_INFINITE_LINES_PREF),
+    "'Extend grid lines infinitely' is pref off by default.");
+
+  info("Toggling ON the 'Extend grid lines infinitely' setting.");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.highlighterSettings.showInfiniteLines);
+  checkbox.click();
+  yield onCheckboxChange;
+
+  info("Toggling OFF the 'Extend grid lines infinitely' setting.");
+  onCheckboxChange = waitUntilState(store, state =>
+    !state.highlighterSettings.showInfiniteLines);
+  checkbox.click();
+  yield onCheckboxChange;
+
+  ok(!Services.prefs.getBoolPref(SHOW_INFINITE_LINES_PREF),
+    "'Extend grid lines infinitely' is pref off.");
+
+  Services.prefs.clearUserPref(SHOW_INFINITE_LINES_PREF);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_display-setting-show-grid-line-numbers.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the 'Display numbers on lines' grid highlighter setting will update
+// the redux store and pref setting.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { store } = inspector;
+
+  yield selectNode("#grid", inspector);
+  let checkbox = doc.getElementById("grid-setting-show-grid-line-numbers");
+
+  info("Checking the initial state of the CSS grid highlighter setting.");
+  ok(!Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+    "'Display numbers on lines' is pref off by default.");
+
+  info("Toggling ON the 'Display numbers on lines' setting.");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.highlighterSettings.showGridLineNumbers);
+  checkbox.click();
+  yield onCheckboxChange;
+
+  ok(Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+    "'Display numbers on lines' is pref on.");
+
+  info("Toggling OFF the 'Display numbers on lines' setting.");
+  onCheckboxChange = waitUntilState(store, state =>
+    !state.highlighterSettings.showGridLineNumbers);
+  checkbox.click();
+  yield onCheckboxChange;
+
+  ok(!Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS),
+    "'Display numbers on lines' is pref off.");
+
+  Services.prefs.clearUserPref(SHOW_GRID_LINE_NUMBERS);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-ESC.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item's color change in the colorpicker is reverted when ESCAPE is
+// pressed.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { store } = inspector;
+  let cPicker = gridInspector.getSwatchColorPickerTooltip();
+  let spectrum = cPicker.spectrum;
+  let swatch = doc.querySelector(".grid-color-swatch");
+
+  info("Checking the initial state of the Grid Inspector.");
+  is(swatch.style.backgroundColor, "rgb(75, 0, 130)",
+    "The color swatch's background is correct.");
+  is(store.getState().grids[0].color, "#4B0082", "The grid color state is correct.");
+
+  info("Scrolling into view of the #grid color swatch.");
+  swatch.scrollIntoView();
+
+  info("Opening the color picker by clicking on the #grid color swatch.");
+  let onColorPickerReady = cPicker.once("ready");
+  swatch.click();
+  yield onColorPickerReady;
+
+  yield simulateColorPickerChange(cPicker, [0, 255, 0, .5]);
+
+  is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+    "The color swatch's background was updated.");
+
+  info("Pressing ESCAPE to close the tooltip.");
+  let onGridColorUpdate = waitUntilState(store, state =>
+    state.grids[0].color === "#4B0082");
+  let onColorPickerHidden = cPicker.tooltip.once("hidden");
+  focusAndSendKey(spectrum.element.ownerDocument.defaultView, "ESCAPE");
+  yield onColorPickerHidden;
+  yield onGridColorUpdate;
+
+  is(swatch.style.backgroundColor, "rgb(75, 0, 130)",
+    "The color swatch's background was reverted after ESCAPE.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-color-picker-on-RETURN.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item's color change in the colorpicker is committed when RETURN is
+// pressed.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { store } = inspector;
+  let cPicker = gridInspector.getSwatchColorPickerTooltip();
+  let spectrum = cPicker.spectrum;
+  let swatch = doc.querySelector(".grid-color-swatch");
+
+  info("Checking the initial state of the Grid Inspector.");
+  is(swatch.style.backgroundColor, "rgb(75, 0, 130)",
+    "The color swatch's background is correct.");
+  is(store.getState().grids[0].color, "#4B0082", "The grid color state is correct.");
+
+  info("Scrolling into view of the #grid color swatch.");
+  swatch.scrollIntoView();
+
+  info("Opening the color picker by clicking on the #grid color swatch.");
+  let onColorPickerReady = cPicker.once("ready");
+  swatch.click();
+  yield onColorPickerReady;
+
+  yield simulateColorPickerChange(cPicker, [0, 255, 0, .5]);
+
+  is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+    "The color swatch's background was updated.");
+
+  info("Pressing RETURN to commit the color change.");
+  let onGridColorUpdate = waitUntilState(store, state =>
+    state.grids[0].color === "#00FF0080");
+  let onColorPickerHidden = cPicker.tooltip.once("hidden");
+  focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
+  yield onColorPickerHidden;
+  yield onGridColorUpdate;
+
+  is(swatch.style.backgroundColor, "rgba(0, 255, 0, 0.5)",
+    "The color swatch's background was kept after RETURN.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item's element rep will display the box model higlighter on hover
+// and select the node on click.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector, toolbox } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { store } = inspector;
+
+  let gridList = doc.querySelector("#grid-list");
+  let elementRep = gridList.children[0].querySelector(".open-inspector");
+  info("Scrolling into the view the #grid element node rep.");
+  elementRep.scrollIntoView();
+
+  info("Listen to node-highlight event and mouse over the widget");
+  let onHighlight = toolbox.once("node-highlight");
+  EventUtils.synthesizeMouse(elementRep, 10, 5, {type: "mouseover"}, doc.defaultView);
+  let nodeFront = yield onHighlight;
+
+  ok(nodeFront, "nodeFront was returned from highlighting the node.");
+  is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName.");
+  is(nodeFront.attributes[0].name, "id",
+    "The highlighted node has the correct attributes.");
+  is(nodeFront.attributes[0].value, "grid", "The highlighted node has the correct id.");
+
+  let onSelection = inspector.selection.once("new-node-front");
+  EventUtils.sendMouseEvent({type: "click"}, elementRep, doc.defaultView);
+  yield onSelection;
+
+  is(inspector.selection.nodeFront, store.getState().grids[0].nodeFront,
+    "The selected node is the one stored on the grid item's state.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-no-grids.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that no grid list items and a "no grids available" message is displayed when
+// there are no grid containers on the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters } = inspector;
+
+  yield selectNode("#grid", inspector);
+  let noGridList = doc.querySelector(".layout-no-grids");
+  let gridList = doc.querySelector("#grid-list");
+
+  info("Checking the initial state of the Grid Inspector.");
+  ok(noGridList, "The message no grid containers is displayed.");
+  ok(!gridList, "No grid containers are listed.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the highlighters overlay.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-added.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid list updates when a new grid container is added to the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid1" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+  <div id="grid2">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector, testActor } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters, store } = inspector;
+
+  yield selectNode("#grid", inspector);
+  let gridList = doc.querySelector("#grid-list");
+  let checkbox1 = gridList.children[0].querySelector("input");
+
+  info("Checking the initial state of the Grid Inspector.");
+  is(gridList.childNodes.length, 1, "One grid container is listed.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the highlighters overlay.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter from the layout panel.");
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  checkbox1.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS grid highlighter is created.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter is created in the highlighters overlay.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Adding the #grid2 container in the content page.");
+  let onGridListUpdate = waitUntilState(store, state =>
+    state.grids.length == 2 &&
+    state.grids[0].highlighted &&
+    !state.grids[1].highlighted);
+  testActor.eval(`
+    content.document.getElementById("grid2").classList.add("grid");
+  `);
+  yield onGridListUpdate;
+
+  info("Checking the new Grid Inspector state.");
+  is(gridList.childNodes.length, 2, "Two grid containers are listed.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter is created in the highlighters overlay.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  let checkbox2 = gridList.children[1].querySelector("input");
+
+  info("Toggling ON the CSS grid highlighter for #grid2.");
+  onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 2 &&
+    !state.grids[0].highlighted &&
+    state.grids[1].highlighted);
+  checkbox2.click();
+  yield onHighlighterShown;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is still shown.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Toggling OFF the CSS grid highlighter from the layout panel.");
+  let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+  onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 2 &&
+    !state.grids[0].highlighted &&
+    !state.grids[1].highlighted);
+  checkbox2.click();
+  yield onHighlighterHidden;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is not shown.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-on-mutation-element-removed.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid item is removed from the grid list when the grid container is
+// removed from the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector, testActor } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters, store } = inspector;
+
+  yield selectNode("#grid", inspector);
+  let gridList = doc.querySelector("#grid-list");
+  let checkbox = gridList.children[0].querySelector("input");
+
+  info("Checking the initial state of the Grid Inspector.");
+  is(gridList.childNodes.length, 1, "One grid container is listed.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the highlighters overlay.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter from the layout panel.");
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 1 && state.grids[0].highlighted);
+  checkbox.click();
+  yield onHighlighterShown;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is created.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter is created in the highlighters overlay.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Removing the #grid container in the content page.");
+  let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+  onCheckboxChange = waitUntilState(store, state => state.grids.length == 0);
+  testActor.eval(`
+    content.document.getElementById("grid").remove();
+  `);
+  yield onHighlighterHidden;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is not shown.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+  let noGridList = doc.querySelector(".layout-no-grids");
+  ok(noGridList, "The message no grid containers is displayed.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-multiple-grids.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the grid inspector panel with multiple grids in
+// the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid1" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+  <div id="grid2" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { inspector, gridInspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters, store } = inspector;
+
+  yield selectNode("#grid1", inspector);
+  let gridList = doc.querySelector("#grid-list");
+  let checkbox1 = gridList.children[0].querySelector("input");
+  let checkbox2 = gridList.children[1].querySelector("input");
+
+  info("Checking the initial state of the Grid Inspector.");
+  is(gridList.childNodes.length, 2, "2 grid containers are listed.");
+  ok(!checkbox1.checked, `Grid item ${checkbox1.value} is unchecked in the grid list.`);
+  ok(!checkbox2.checked, `Grid item ${checkbox2.value} is unchecked in the grid list.`);
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the highlighters overlay.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter for #grid1.");
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 2 &&
+    state.grids[0].highlighted &&
+    !state.grids[1].highlighted);
+  checkbox1.click();
+  yield onHighlighterShown;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is created.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter is created in the highlighters overlay.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter for #grid2.");
+  onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 2 &&
+    !state.grids[0].highlighted &&
+    state.grids[1].highlighted);
+  checkbox2.click();
+  yield onHighlighterShown;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is still shown.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Toggling OFF the CSS grid highlighter from the layout panel.");
+  let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+  onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 2 &&
+    !state.grids[0].highlighted &&
+    !state.grids[1].highlighted);
+  checkbox2.click();
+  yield onHighlighterHidden;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is not shown.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-toggle-single-grid.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests toggling ON/OFF the grid highlighter from the grid ispector panel.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let { gridInspector, inspector } = yield openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters, store } = inspector;
+
+  yield selectNode("#grid", inspector);
+  let gridList = doc.querySelector("#grid-list");
+  let checkbox = gridList.children[0].querySelector("input");
+
+  info("Checking the initial state of the Grid Inspector.");
+  is(gridList.childNodes.length, 1, "One grid container is listed.");
+  ok(!checkbox.checked, `Grid item ${checkbox.value} is unchecked in the grid list.`);
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the highlighters overlay.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter from the layout panel.");
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 1 &&
+    state.grids[0].highlighted);
+  checkbox.click();
+  yield onHighlighterShown;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is created.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter is created in the highlighters overlay.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Toggling OFF the CSS grid highlighter from the layout panel.");
+  let onHighlighterHidden = highlighters.once("grid-highlighter-hidden");
+  onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 1 &&
+    !state.grids[0].highlighted);
+  checkbox.click();
+  yield onHighlighterHidden;
+  yield onCheckboxChange;
+
+  info("Checking the CSS grid highlighter is not shown.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/head.js
@@ -0,0 +1,74 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../framework/test/shared-head.js */
+/* import-globals-from ../../test/head.js */
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+  this);
+
+// Load the shared Redux helpers into this compartment.
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
+  this);
+
+Services.prefs.setBoolPref("devtools.layoutview.enabled", true);
+Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.layoutview.enabled");
+  Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+});
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the layout view
+ * sidebar tab selected to display the box model view with properties.
+ *
+ * @return {Promise} a promise that resolves when the inspector is ready and the box model
+ *         view is visible and ready.
+ */
+function openLayoutView() {
+  return openInspectorSidebarTab("layoutview").then(data => {
+    // The actual highligher show/hide methods are mocked in box model tests.
+    // The highlighter is tested in devtools/inspector/test.
+    function mockHighlighter({highlighter}) {
+      highlighter.showBoxModel = function () {
+        return promise.resolve();
+      };
+      highlighter.hideBoxModel = function () {
+        return promise.resolve();
+      };
+    }
+    mockHighlighter(data.toolbox);
+
+    return {
+      toolbox: data.toolbox,
+      inspector: data.inspector,
+      gridInspector: data.inspector.gridInspector,
+      testActor: data.testActor
+    };
+  });
+}
+
+/**
+ * Simulate a color change in a given color picker tooltip.
+ *
+ * @param  {Spectrum|ColorWidget} colorPicker
+ *         The color picker widget.
+ * @param  {Array} newRgba
+ *         Array of the new rgba values to be set in the color widget.
+ */
+var simulateColorPickerChange = Task.async(function* (colorPicker, newRgba) {
+  info("Getting the spectrum colorpicker object");
+  let spectrum = yield colorPicker.spectrum;
+  info("Setting the new color");
+  spectrum.rgb = newRgba;
+  info("Applying the change");
+  spectrum.updateUI();
+  spectrum.onChange();
+});
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -493,26 +493,16 @@ function waitForStyleModification(inspec
  */
 function* clickSelectorIcon(icon, view) {
   let onToggled = view.once("ruleview-selectorhighlighter-toggled");
   EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
   yield onToggled;
 }
 
 /**
- * Make sure window is properly focused before sending a key event.
- * @param {Window} win
- * @param {Event} key
- */
-function focusAndSendKey(win, key) {
-  win.document.documentElement.focus();
-  EventUtils.sendKey(key, win);
-}
-
-/**
  * Toggle one of the checkboxes inside the class-panel. Resolved after the DOM mutation
  * has been recorded.
  * @param {CssRuleView} view The rule-view instance.
  * @param {String} name The class name to find the checkbox.
  */
 function* toggleClassPanelCheckBox(view, name) {
   info(`Clicking on checkbox for class ${name}`);
   const checkBox = [...view.classPanel.querySelectorAll("[type=checkbox]")].find(box => {
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -1,17 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const promise = require("promise");
 const {Task} = require("devtools/shared/task");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types");
 
 const DEFAULT_GRID_COLOR = "#4B0082";
 
 /**
  * Highlighters overlay is a singleton managing all highlighters in the Inspector.
@@ -262,28 +261,36 @@ HighlightersOverlay.prototype = {
 
   /**
    * Get a highlighter front given a type. It will only be initialized once.
    *
    * @param  {String} type
    *         The highlighter type. One of this.highlighters.
    * @return {Promise} that resolves to the highlighter
    */
-  _getHighlighter: function (type) {
+  _getHighlighter: Task.async(function* (type) {
     let utils = this.highlighterUtils;
 
     if (this.highlighters[type]) {
-      return promise.resolve(this.highlighters[type]);
+      return this.highlighters[type];
     }
 
-    return utils.getHighlighterByType(type).then(highlighter => {
-      this.highlighters[type] = highlighter;
-      return highlighter;
-    });
-  },
+    let highlighter;
+
+    try {
+      highlighter = yield utils.getHighlighterByType(type);
+    } catch (e) {}
+
+    if (!highlighter) {
+      return null;
+    }
+
+    this.highlighters[type] = highlighter;
+    return highlighter;
+  }),
 
   _handleRejection: function (error) {
     if (!this.destroyed) {
       console.error(error);
     }
   },
 
   /**
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -639,16 +639,29 @@ var waitForTab = Task.async(function* ()
  */
 function synthesizeKeys(input, win) {
   for (let key of input.split("")) {
     EventUtils.synthesizeKey(key, {}, win);
   }
 }
 
 /**
+ * Make sure window is properly focused before sending a key event.
+ *
+ * @param {Window} win
+ *        The window containing the panel
+ * @param {String} key
+ *        The string value to input
+ */
+function focusAndSendKey(win, key) {
+  win.document.documentElement.focus();
+  EventUtils.sendKey(key, win);
+}
+
+/**
  * Given a Tooltip instance, fake a mouse event on the `target` DOM Element
  * and assert that the `tooltip` is correctly displayed.
  *
  * @param {Tooltip} tooltip
  *        The tooltip instance
  * @param {DOMElement} target
  *        The DOM Element on which a tooltip should appear
  *