merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 05 Jul 2016 16:02:17 +0200
changeset 303694 b2e48cc9d3a02fc172910ce8b29f83c19db20777
parent 303686 f47a1bc057ff5a6084e776040425da258a2851ca (current diff)
parent 303693 652fa69526ff82afbe7b7e6a360709c457fddf19 (diff)
child 303695 dbb31bcad5a1f60a35b5600ea1578d9b9fa55237
push id79141
push usercbook@mozilla.com
push dateTue, 05 Jul 2016 14:07:42 +0000
treeherdermozilla-inbound@f08c54971dd1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/devtools/client/debugger/debugger-view.js
+++ b/devtools/client/debugger/debugger-view.js
@@ -654,17 +654,17 @@ var DebuggerView = {
     }));
   },
 
   /**
    * Gets the visibility state of the instruments pane.
    * @return boolean
    */
   get instrumentsPaneHidden() {
-    return this._instrumentsPane.hasAttribute("pane-collapsed");
+    return this._instrumentsPane.classList.contains("pane-collapsed");
   },
 
   /**
    * Gets the currently selected tab in the instruments pane.
    * @return string
    */
   get instrumentsPaneTab() {
     return this._instrumentsPane.selectedTab.id;
@@ -684,20 +684,20 @@ var DebuggerView = {
    */
   toggleInstrumentsPane: function (aFlags, aTabIndex) {
     let pane = this._instrumentsPane;
     let button = this._instrumentsPaneToggleButton;
 
     ViewHelpers.togglePane(aFlags, pane);
 
     if (aFlags.visible) {
-      button.removeAttribute("pane-collapsed");
+      button.classList.remove("pane-collapsed");
       button.setAttribute("tooltiptext", this._collapsePaneString);
     } else {
-      button.setAttribute("pane-collapsed", "");
+      button.classList.add("pane-collapsed");
       button.setAttribute("tooltiptext", this._expandPaneString);
     }
 
     if (aTabIndex !== undefined) {
       pane.selectedIndex = aTabIndex;
     }
   },
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_instruments-pane-collapse.js
@@ -36,18 +36,18 @@ function test() {
 }
 
 function testPanesState() {
   let instrumentsPane =
     gDebugger.document.getElementById("instruments-pane");
   let instrumentsPaneToggleButton =
     gDebugger.document.getElementById("instruments-pane-toggle");
 
-  ok(instrumentsPane.hasAttribute("pane-collapsed") &&
-     instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(instrumentsPane.classList.contains("pane-collapsed") &&
+     instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The debugger view instruments pane should initially be hidden.");
   is(gPrefs.panesVisibleOnStartup, false,
     "The debugger view instruments pane should initially be preffed as hidden.");
   isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should not be checked.");
 }
 
 function testInstrumentsPaneCollapse() {
@@ -60,18 +60,18 @@ function testInstrumentsPaneCollapse() {
   is(width, gPrefs.instrumentsWidth,
     "The instruments pane has an incorrect width.");
   is(instrumentsPane.style.marginLeft, "0px",
     "The instruments pane has an incorrect left margin.");
   is(instrumentsPane.style.marginRight, "0px",
     "The instruments pane has an incorrect right margin.");
   ok(!instrumentsPane.hasAttribute("animated"),
     "The instruments pane has an incorrect animated attribute.");
-  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
-     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+     !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The instruments pane should at this point be visible.");
 
   gDebugger.DebuggerView.toggleInstrumentsPane({ visible: false, animated: true });
 
   is(gPrefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
   isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
@@ -80,18 +80,18 @@ function testInstrumentsPaneCollapse() {
   is(width, gPrefs.instrumentsWidth,
     "The instruments pane has an incorrect width after collapsing.");
   is(instrumentsPane.style.marginLeft, margin,
     "The instruments pane has an incorrect left margin after collapsing.");
   is(instrumentsPane.style.marginRight, margin,
     "The instruments pane has an incorrect right margin after collapsing.");
   ok(instrumentsPane.hasAttribute("animated"),
     "The instruments pane has an incorrect attribute after an animated collapsing.");
-  ok(instrumentsPane.hasAttribute("pane-collapsed") &&
-     instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(instrumentsPane.classList.contains("pane-collapsed") &&
+     instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The instruments pane should not be visible after collapsing.");
 
   gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
 
   is(gPrefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
   isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
@@ -99,54 +99,54 @@ function testInstrumentsPaneCollapse() {
   is(width, gPrefs.instrumentsWidth,
     "The instruments pane has an incorrect width after uncollapsing.");
   is(instrumentsPane.style.marginLeft, "0px",
     "The instruments pane has an incorrect left margin after uncollapsing.");
   is(instrumentsPane.style.marginRight, "0px",
     "The instruments pane has an incorrect right margin after uncollapsing.");
   ok(!instrumentsPane.hasAttribute("animated"),
     "The instruments pane has an incorrect attribute after an unanimated uncollapsing.");
-  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
-     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+     !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The instruments pane should be visible again after uncollapsing.");
 }
 
 function testPanesStartupPref() {
   let instrumentsPane =
     gDebugger.document.getElementById("instruments-pane");
   let instrumentsPaneToggleButton =
     gDebugger.document.getElementById("instruments-pane-toggle");
 
   is(gPrefs.panesVisibleOnStartup, false,
     "The debugger view panes should still initially be preffed as hidden.");
 
-  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
-     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+     !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The debugger instruments pane should at this point be visible.");
   is(gPrefs.panesVisibleOnStartup, false,
     "The debugger view panes should initially be preffed as hidden.");
   isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should still not be checked.");
 
   gOptions._showPanesOnStartupItem.setAttribute("checked", "true");
   gOptions._toggleShowPanesOnStartup();
 
-  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
-     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+     !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The debugger instruments pane should at this point be visible.");
   is(gPrefs.panesVisibleOnStartup, true,
     "The debugger view panes should now be preffed as visible.");
   is(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should now be checked.");
 
   gOptions._showPanesOnStartupItem.setAttribute("checked", "false");
   gOptions._toggleShowPanesOnStartup();
 
-  ok(!instrumentsPane.hasAttribute("pane-collapsed") &&
-     !instrumentsPaneToggleButton.hasAttribute("pane-collapsed"),
+  ok(!instrumentsPane.classList.contains("pane-collapsed") &&
+     !instrumentsPaneToggleButton.classList.contains("pane-collapsed"),
     "The debugger instruments pane should at this point be visible.");
   is(gPrefs.panesVisibleOnStartup, false,
     "The debugger view panes should now be preffed as hidden.");
   isnot(gOptions._showPanesOnStartupItem.getAttribute("checked"), "true",
     "The options menu item should now be unchecked.");
 }
 
 registerCleanupFunction(function () {
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -378,16 +378,28 @@ InspectorPanel.prototype = {
       } else {
         str = strings.GetStringFromName("inspector.searchResultsNone");
       }
     }
 
     this.searchResultsLabel.textContent = str;
   },
 
+  get React() {
+    return this._toolbox.React;
+  },
+
+  get ReactDOM() {
+    return this._toolbox.ReactDOM;
+  },
+
+  get browserRequire() {
+    return this._toolbox.browserRequire;
+  },
+
   /**
    * Build the sidebar.
    */
   setupSidebar: function () {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
       showAllTabsMenu: true
     });
@@ -426,21 +438,28 @@ InspectorPanel.prototype = {
 
     this.setupSidebarToggle();
   },
 
   /**
    * Add the expand/collapse behavior for the sidebar panel.
    */
   setupSidebarToggle: function () {
-    this._paneToggleButton = this.panelDoc.getElementById("inspector-pane-toggle");
-    this._paneToggleButton.setAttribute("tooltiptext",
-      strings.GetStringFromName("inspector.collapsePane"));
-    this._paneToggleButton.addEventListener("mousedown",
-      this.onPaneToggleButtonClicked);
+    let SidebarToggle = this.React.createFactory(this.browserRequire(
+      "devtools/client/shared/components/sidebar-toggle"));
+
+    let sidebarToggle = SidebarToggle({
+      onClick: this.onPaneToggleButtonClicked,
+      collapsed: false,
+      expandPaneTitle: strings.GetStringFromName("inspector.expandPane"),
+      collapsePaneTitle: strings.GetStringFromName("inspector.collapsePane"),
+    });
+
+    let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box");
+    this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox);
   },
 
   /**
    * Reset the inspector on new root mutation.
    */
   onNewRoot: function () {
     this._defaultNode = null;
     this.selection.setNodeFront(null);
@@ -684,19 +703,16 @@ InspectorPanel.prototype = {
     });
 
     this.sidebar.off("select", this._setDefaultSidebar);
     let sidebarDestroyer = this.sidebar.destroy();
     this.sidebar = null;
 
     this.addNodeButton.removeEventListener("click", this.addNode);
     this.breadcrumbs.destroy();
-    this._paneToggleButton.removeEventListener("mousedown",
-      this.onPaneToggleButtonClicked);
-    this._paneToggleButton = null;
     this.selection.off("new-node-front", this.onNewSelection);
     this.selection.off("before-new-node", this.onBeforeNewSelection);
     this.selection.off("before-new-node-front", this.onBeforeNewSelection);
     this.selection.off("detached-front", this.onDetached);
     let markupDestroyer = this._destroyMarkup();
     this.panelWin.inspector = null;
     this.target = null;
     this.panelDoc = null;
@@ -1138,18 +1154,17 @@ InspectorPanel.prototype = {
   },
 
   /**
    * When the pane toggle button is clicked, toggle the pane, change the button
    * state and tooltip.
    */
   onPaneToggleButtonClicked: function (e) {
     let sidePane = this.panelDoc.querySelector("#inspector-sidebar");
-    let button = this._paneToggleButton;
-    let isVisible = !button.hasAttribute("pane-collapsed");
+    let isVisible = !this._sidebarToggle.state.collapsed;
 
     // Make sure the sidebar has width and height attributes before collapsing
     // because ViewHelpers needs it.
     if (isVisible) {
       let rect = sidePane.getBoundingClientRect();
       if (!sidePane.hasAttribute("width")) {
         sidePane.setAttribute("width", rect.width);
       }
@@ -1160,21 +1175,19 @@ InspectorPanel.prototype = {
 
     ViewHelpers.togglePane({
       visible: !isVisible,
       animated: true,
       delayed: true
     }, sidePane);
 
     if (isVisible) {
-      button.setAttribute("pane-collapsed", "");
-      button.setAttribute("tooltiptext", strings.GetStringFromName("inspector.expandPane"));
+      this._sidebarToggle.setState({collapsed: true});
     } else {
-      button.removeAttribute("pane-collapsed");
-      button.setAttribute("tooltiptext", strings.GetStringFromName("inspector.collapsePane"));
+      this._sidebarToggle.setState({collapsed: false});
     }
   },
 
   /**
    * Create a new node as the last child of the current selection, expand the
    * parent and select the new node.
    */
   addNode: Task.async(function* () {
--- a/devtools/client/inspector/inspector.css
+++ b/devtools/client/inspector/inspector.css
@@ -27,9 +27,13 @@
   max-width: 150px;
 }
 
 .inspector-tabpanel > * {
   /*
    * Override `-moz-user-focus:ignore;` from toolkit/content/minimal-xul.css
    */
   -moz-user-focus: normal;
-}
\ No newline at end of file
+}
+
+#inspector-sidebar-toggle-box {
+  line-height: initial;
+}
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -6,16 +6,17 @@
 <?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/content/inspector/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/inspector.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/rules.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/computed.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/fonts.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/layout.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/sidebar-toggle.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % inspectorDTD SYSTEM "chrome://devtools/locale/inspector.dtd"> %inspectorDTD;
   <!ENTITY % styleinspectorDTD SYSTEM "chrome://devtools/locale/styleinspector.dtd"> %styleinspectorDTD;
   <!ENTITY % fontinspectorDTD SYSTEM "chrome://devtools/locale/font-inspector.dtd"> %fontinspectorDTD;
   <!ENTITY % layoutviewDTD SYSTEM "chrome://devtools/locale/layoutview.dtd"> %layoutviewDTD;
 ]>
 
@@ -34,19 +35,18 @@
           class="devtools-button" />
         <html:div class="devtools-toolbar-spacer" />
         <html:span id="inspector-searchlabel" />
         <textbox id="inspector-searchbox"
           type="search"
           timeout="50"
           class="devtools-searchinput"
           placeholder="&inspectorSearchHTML.label3;"/>
-        <html:button id="inspector-pane-toggle"
-          class="devtools-button"
-          tabindex="0" />
+        <div xmlns="http://www.w3.org/1999/xhtml"
+             id="inspector-sidebar-toggle-box" />
       </html:div>
       <vbox flex="1" id="markup-box">
       </vbox>
       <html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
         <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"/>
       </html:div>
     </vbox>
     <splitter class="devtools-side-splitter"/>
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-01.js
@@ -5,23 +5,23 @@
 
 // Test that the inspector panel has a sidebar pane toggle button, and that
 // this button is visible both in BOTTOM and SIDE hosts.
 
 add_task(function* () {
   info("Open the inspector in a bottom toolbox host");
   let {toolbox, inspector} = yield openInspectorForURL("about:blank", "bottom");
 
-  let button = inspector.panelDoc.getElementById("inspector-pane-toggle");
+  let button = inspector.panelDoc.querySelector(".sidebar-toggle");
   ok(button, "The toggle button exists in the DOM");
-  is(button.parentNode.id, "inspector-toolbar",
-     "The toggle button is in the toolbar");
-  ok(button.getAttribute("tooltiptext"), "The tool tip has initial state");
-  ok(!button.hasAttribute("pane-collapsed"), "The button is in expanded state");
+  is(button.parentNode.id, "inspector-sidebar-toggle-box",
+     "The toggle button has the right parent");
+  ok(button.getAttribute("title"), "The tool tip has initial state");
+  ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
   ok(!!button.getClientRects().length, "The button is visible");
 
   info("Switch the host to side type");
   yield toolbox.switchHost("side");
 
   ok(!!button.getClientRects().length, "The button is still visible");
-  ok(!button.hasAttribute("pane-collapsed"),
+  ok(!button.classList.contains("pane-collapsed"),
      "The button is still in expanded state");
 });
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-02.js
@@ -7,36 +7,36 @@
 // clicking on the toggle button and remains expanded/collapsed when switching
 // hosts.
 
 add_task(function* () {
   info("Open the inspector in a side toolbox host");
   let {toolbox, inspector} = yield openInspectorForURL("about:blank", "side");
 
   let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
-  let button = inspector.panelDoc.getElementById("inspector-pane-toggle");
-  ok(!panel.hasAttribute("pane-collapsed"), "The panel is in expanded state");
+  let button = inspector.panelDoc.querySelector(".sidebar-toggle");
+  ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 
   info("Listen to the end of the animation on the sidebar panel");
   let onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button");
-  EventUtils.synthesizeMouseAtCenter(button, {type: "mousedown"},
+  EventUtils.synthesizeMouseAtCenter(button, {},
     inspector.panelDoc.defaultView);
 
   yield onTransitionEnd;
-  ok(panel.hasAttribute("pane-collapsed"), "The panel is in collapsed state");
+  ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
   ok(!panel.hasAttribute("animated"),
     "The collapsed panel will not perform unwanted animations");
 
   info("Switch the host to bottom type");
   yield toolbox.switchHost("bottom");
-  ok(panel.hasAttribute("pane-collapsed"), "The panel is in collapsed state");
+  ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
 
   info("Click on the toggle button to expand the panel again");
 
   onTransitionEnd = once(panel, "transitionend");
-  EventUtils.synthesizeMouseAtCenter(button, {type: "mousedown"},
+  EventUtils.synthesizeMouseAtCenter(button, {},
     inspector.panelDoc.defaultView);
   yield onTransitionEnd;
 
-  ok(!panel.hasAttribute("pane-collapsed"), "The panel is in expanded state");
+  ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 });
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-03.js
@@ -4,35 +4,35 @@
 "use strict";
 
 // Test that the toggle button can collapse and expand the inspector side/bottom
 // panel, and that the appropriate attributes are updated in the process.
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL("about:blank");
 
-  let button = inspector.panelDoc.getElementById("inspector-pane-toggle");
+  let button = inspector.panelDoc.querySelector(".sidebar-toggle");
   let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
 
-  ok(!button.hasAttribute("pane-collapsed"), "The button is in expanded state");
+  ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
 
   info("Listen to the end of the animation on the sidebar panel");
   let onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button");
-  EventUtils.synthesizeMouseAtCenter(button, {type: "mousedown"},
+  EventUtils.synthesizeMouseAtCenter(button, {},
     inspector.panelDoc.defaultView);
 
   yield onTransitionEnd;
-  ok(button.hasAttribute("pane-collapsed"), "The button is in collapsed state");
-  ok(panel.hasAttribute("pane-collapsed"), "The panel is in collapsed state");
+  ok(button.classList.contains("pane-collapsed"), "The button is in collapsed state");
+  ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
 
   info("Listen again to the end of the animation on the sidebar panel");
   onTransitionEnd = once(panel, "transitionend");
 
   info("Click on the toggle button again");
-  EventUtils.synthesizeMouseAtCenter(button, {type: "mousedown"},
+  EventUtils.synthesizeMouseAtCenter(button, {},
     inspector.panelDoc.defaultView);
 
   yield onTransitionEnd;
-  ok(!button.hasAttribute("pane-collapsed"), "The button is in expanded state");
-  ok(!panel.hasAttribute("pane-collapsed"), "The panel is in expanded state");
+  ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");
+  ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 });
--- a/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
+++ b/devtools/client/inspector/test/browser_inspector_pane-toggle-04.js
@@ -14,47 +14,47 @@ add_task(function* () {
   yield new Promise(resolve => {
     let options = {"set": [
       ["devtools.toolsidebar-width.inspector", 200]
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { inspector, toolbox } = yield openInspectorForURL("about:blank");
-  let button = inspector.panelDoc.getElementById("inspector-pane-toggle");
+  let button = inspector.panelDoc.querySelector(".sidebar-toggle");
   let panel = inspector.panelDoc.querySelector("#inspector-sidebar");
 
   info("Changing toolbox host to a window.");
   yield toolbox.switchHost(Toolbox.HostType.WINDOW);
 
   let hostWindow = toolbox._host._window;
   let originalWidth = hostWindow.outerWidth;
   let originalHeight = hostWindow.outerHeight;
 
   info("Resizing window to switch to the horizontal layout.");
   hostWindow.resizeTo(800, 300);
 
   // Check the sidebar is expanded when the test starts.
-  ok(!panel.hasAttribute("pane-collapsed"), "The panel is in expanded state");
+  ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
 
   info("Collapse the inspector sidebar.");
   let onTransitionEnd = once(panel, "transitionend");
-  EventUtils.synthesizeMouseAtCenter(button, {type: "mousedown"},
+  EventUtils.synthesizeMouseAtCenter(button, {},
     inspector.panelDoc.defaultView);
   yield onTransitionEnd;
 
-  ok(panel.hasAttribute("pane-collapsed"), "The panel is in collapsed state");
+  ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
   let currentPanelHeight = panel.getBoundingClientRect().height;
   let currentPanelMarginBottom = panel.style.marginBottom;
 
   info("Resizing window to switch to the vertical layout.");
   hostWindow.resizeTo(300, 800);
 
   // Check the panel is collapsed, and still has the same dimensions.
-  ok(panel.hasAttribute("pane-collapsed"), "The panel is still collapsed");
+  ok(panel.classList.contains("pane-collapsed"), "The panel is still collapsed");
   is(panel.getBoundingClientRect().height, currentPanelHeight,
     "The panel height has not been modified when changing the layout.");
   is(panel.style.marginBottom, currentPanelMarginBottom,
     "The panel margin-bottom has not been modified when changing the layout.");
 
   info("Restoring window original size.");
   hostWindow.resizeTo(originalWidth, originalHeight);
 });
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -245,17 +245,17 @@ var NetMonitorView = {
     }
   }),
 
   /**
    * Gets the visibility state of the network details pane.
    * @return boolean
    */
   get detailsPaneHidden() {
-    return this._detailsPane.hasAttribute("pane-collapsed");
+    return this._detailsPane.classList.contains("pane-collapsed");
   },
 
   /**
    * Sets the network details pane hidden or visible.
    *
    * @param object flags
    *        An object containing some of the following properties:
    *        - visible: true if the pane should be shown, false to hide
@@ -267,22 +267,22 @@ var NetMonitorView = {
    */
   toggleDetailsPane: function (flags, tabIndex) {
     let pane = this._detailsPane;
     let button = this._detailsPaneToggleButton;
 
     ViewHelpers.togglePane(flags, pane);
 
     if (flags.visible) {
-      this._body.removeAttribute("pane-collapsed");
-      button.removeAttribute("pane-collapsed");
+      this._body.classList.remove("pane-collapsed");
+      button.classList.remove("pane-collapsed");
       button.setAttribute("tooltiptext", this._collapsePaneString);
     } else {
-      this._body.setAttribute("pane-collapsed", "");
-      button.setAttribute("pane-collapsed", "");
+      this._body.classList.add("pane-collapsed");
+      button.classList.add("pane-collapsed");
       button.setAttribute("tooltiptext", this._expandPaneString);
     }
 
     if (tabIndex !== undefined) {
       $("#event-details-pane").selectedIndex = tabIndex;
     }
   },
 
--- a/devtools/client/netmonitor/netmonitor.css
+++ b/devtools/client/netmonitor/netmonitor.css
@@ -31,14 +31,14 @@
   /* workaround for textbox not supporting the @crop attribute */
   text-overflow: ellipsis;
 }
 
 /* Responsive sidebar */
 @media (max-width: 700px) {
   #toolbar-spacer,
   #details-pane-toggle,
-  #details-pane[pane-collapsed],
+  #details-pane.pane-collapsed,
   .requests-menu-waterfall,
   #requests-menu-network-summary-button > .toolbarbutton-text {
     display: none;
   }
 }
--- a/devtools/client/netmonitor/test/browser_net_clear.js
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -29,25 +29,25 @@ function test() {
       assertNoRequestState(RequestsMenu, detailsPaneToggleButton);
 
       // Load a second request and make sure they still show up
       aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.NETWORK_EVENT, () => {
         assertSingleRequestState(RequestsMenu, detailsPaneToggleButton);
 
         // Make sure we can now open the details pane
         NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
-        ok(!detailsPane.hasAttribute("pane-collapsed") &&
-          !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+        ok(!detailsPane.classList.contains("pane-collapsed") &&
+          !detailsPaneToggleButton.classList.contains("pane-collapsed"),
           "The details pane should be visible after clicking the toggle button.");
 
         // Click clear and make sure the details pane closes
         EventUtils.sendMouseEvent({ type: "click" }, clearButton);
         assertNoRequestState(RequestsMenu, detailsPaneToggleButton);
-        ok(detailsPane.hasAttribute("pane-collapsed") &&
-          detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+        ok(detailsPane.classList.contains("pane-collapsed") &&
+          detailsPaneToggleButton.classList.contains("pane-collapsed"),
           "The details pane should not be visible clicking 'clear'.");
 
         teardown(aMonitor).then(finish);
       });
 
       aDebuggee.location.reload();
     });
 
--- a/devtools/client/netmonitor/test/browser_net_pane-collapse.js
+++ b/devtools/client/netmonitor/test/browser_net_pane-collapse.js
@@ -8,59 +8,59 @@
 function test() {
   initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     let { document, Prefs, NetMonitorView } = aMonitor.panelWin;
     let detailsPane = document.getElementById("details-pane");
     let detailsPaneToggleButton = document.getElementById("details-pane-toggle");
 
-    ok(detailsPane.hasAttribute("pane-collapsed") &&
-       detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+    ok(detailsPane.classList.contains("pane-collapsed") &&
+       detailsPaneToggleButton.classList.contains("pane-collapsed"),
       "The details pane should initially be hidden.");
 
     NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
 
     let width = ~~(detailsPane.getAttribute("width"));
     is(width, Prefs.networkDetailsWidth,
       "The details pane has an incorrect width.");
     is(detailsPane.style.marginLeft, "0px",
       "The details pane has an incorrect left margin.");
     is(detailsPane.style.marginRight, "0px",
       "The details pane has an incorrect right margin.");
     ok(!detailsPane.hasAttribute("animated"),
       "The details pane has an incorrect animated attribute.");
-    ok(!detailsPane.hasAttribute("pane-collapsed") &&
-       !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+    ok(!detailsPane.classList.contains("pane-collapsed") &&
+       !detailsPaneToggleButton.classList.contains("pane-collapsed"),
       "The details pane should at this point be visible.");
 
     NetMonitorView.toggleDetailsPane({ visible: false, animated: true });
 
     let margin = -(width + 1) + "px";
     is(width, Prefs.networkDetailsWidth,
       "The details pane has an incorrect width after collapsing.");
     is(detailsPane.style.marginLeft, margin,
       "The details pane has an incorrect left margin after collapsing.");
     is(detailsPane.style.marginRight, margin,
       "The details pane has an incorrect right margin after collapsing.");
     ok(detailsPane.hasAttribute("animated"),
       "The details pane has an incorrect attribute after an animated collapsing.");
-    ok(detailsPane.hasAttribute("pane-collapsed") &&
-       detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+    ok(detailsPane.classList.contains("pane-collapsed") &&
+       detailsPaneToggleButton.classList.contains("pane-collapsed"),
       "The details pane should not be visible after collapsing.");
 
     NetMonitorView.toggleDetailsPane({ visible: true, animated: false });
 
     is(width, Prefs.networkDetailsWidth,
       "The details pane has an incorrect width after uncollapsing.");
     is(detailsPane.style.marginLeft, "0px",
       "The details pane has an incorrect left margin after uncollapsing.");
     is(detailsPane.style.marginRight, "0px",
       "The details pane has an incorrect right margin after uncollapsing.");
     ok(!detailsPane.hasAttribute("animated"),
       "The details pane has an incorrect attribute after an unanimated uncollapsing.");
-    ok(!detailsPane.hasAttribute("pane-collapsed") &&
-       !detailsPaneToggleButton.hasAttribute("pane-collapsed"),
+    ok(!detailsPane.classList.contains("pane-collapsed") &&
+       !detailsPaneToggleButton.classList.contains("pane-collapsed"),
       "The details pane should be visible again after uncollapsing.");
 
     teardown(aMonitor).then(finish);
   });
 }
--- a/devtools/client/netmonitor/test/browser_net_pane-toggle.js
+++ b/devtools/client/netmonitor/test/browser_net_pane-toggle.js
@@ -13,62 +13,62 @@ function test() {
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     is(document.querySelector("#details-pane-toggle")
       .hasAttribute("disabled"), true,
       "The pane toggle button should be disabled when the frontend is opened.");
     is(document.querySelector("#details-pane-toggle")
-      .hasAttribute("pane-collapsed"), true,
+      .classList.contains("pane-collapsed"), true,
       "The pane toggle button should indicate that the details pane is " +
       "collapsed when the frontend is opened.");
     is(NetMonitorView.detailsPaneHidden, true,
       "The details pane should be hidden when the frontend is opened.");
     is(RequestsMenu.selectedItem, null,
       "There should be no selected item in the requests menu.");
 
     aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.NETWORK_EVENT, () => {
       is(document.querySelector("#details-pane-toggle")
         .hasAttribute("disabled"), false,
         "The pane toggle button should be enabled after the first request.");
       is(document.querySelector("#details-pane-toggle")
-        .hasAttribute("pane-collapsed"), true,
+        .classList.contains("pane-collapsed"), true,
         "The pane toggle button should still indicate that the details pane is " +
         "collapsed after the first request.");
       is(NetMonitorView.detailsPaneHidden, true,
         "The details pane should still be hidden after the first request.");
       is(RequestsMenu.selectedItem, null,
         "There should still be no selected item in the requests menu.");
 
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.getElementById("details-pane-toggle"));
 
       is(document.querySelector("#details-pane-toggle")
         .hasAttribute("disabled"), false,
         "The pane toggle button should still be enabled after being pressed.");
       is(document.querySelector("#details-pane-toggle")
-        .hasAttribute("pane-collapsed"), false,
+        .classList.contains("pane-collapsed"), false,
         "The pane toggle button should now indicate that the details pane is " +
         "not collapsed anymore after being pressed.");
       is(NetMonitorView.detailsPaneHidden, false,
         "The details pane should not be hidden after toggle button was pressed.");
       isnot(RequestsMenu.selectedItem, null,
         "There should be a selected item in the requests menu.");
       is(RequestsMenu.selectedIndex, 0,
         "The first item should be selected in the requests menu.");
 
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.getElementById("details-pane-toggle"));
 
       is(document.querySelector("#details-pane-toggle")
         .hasAttribute("disabled"), false,
         "The pane toggle button should still be enabled after being pressed again.");
       is(document.querySelector("#details-pane-toggle")
-        .hasAttribute("pane-collapsed"), true,
+        .classList.contains("pane-collapsed"), true,
         "The pane toggle button should now indicate that the details pane is " +
         "collapsed after being pressed again.");
       is(NetMonitorView.detailsPaneHidden, true,
         "The details pane should now be hidden after the toggle button was pressed again.");
       is(RequestsMenu.selectedItem, null,
         "There should now be no selected item in the requests menu.");
 
       teardown(aMonitor).then(finish);
--- a/devtools/client/projecteditor/lib/projecteditor.js
+++ b/devtools/client/projecteditor/lib/projecteditor.js
@@ -701,17 +701,17 @@ var ProjectEditor = Class({
 
     // If no plugin wants to handle it, just use a string from the resource.
     if (!renderedByPlugin) {
       elt.textContent = resource.displayName;
     }
   },
 
   get sourcesVisible() {
-    return this.sourceToggle.hasAttribute("pane-collapsed");
+    return this.sourceToggle.classList.contains("pane-collapsed");
   },
 
   get currentShell() {
     return this.shells.currentShell;
   },
 
   get currentEditor() {
     return this.shells.currentEditor;
--- a/devtools/client/shared/components/moz.build
+++ b/devtools/client/shared/components/moz.build
@@ -10,14 +10,16 @@ DIRS += [
     'tree'
 ]
 
 DevToolsModules(
     'frame.js',
     'h-split-box.js',
     'notification-box.css',
     'notification-box.js',
+    'sidebar-toggle.css',
+    'sidebar-toggle.js',
     'stack-trace.js',
     'tree.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.css
@@ -0,0 +1,24 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.sidebar-toggle {
+  display: block;
+}
+
+.sidebar-toggle::before {
+  background-image: var(--theme-pane-collapse-image);
+}
+
+.sidebar-toggle.pane-collapsed::before {
+  background-image: var(--theme-pane-expand-image);
+}
+
+/* Rotate button icon 90deg if the toolbox container is
+  in vertical mode (sidebar displayed under the main panel) */
+@media (max-width: 700px) {
+  .sidebar-toggle::before {
+    transform: rotate(90deg);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript 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 { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Sidebar toggle button. This button is used to exapand
+ * and collapse Sidebar.
+ */
+var SidebarToggle = createClass({
+  displayName: "SidebarToggle",
+
+  propTypes: {
+    // Set to true if collapsed.
+    collapsed: PropTypes.bool.isRequired,
+    // Tooltip text used when the button indicates expanded state.
+    collapsePaneTitle: PropTypes.string.isRequired,
+    // Tooltip text used when the button indicates collapsed state.
+    expandPaneTitle: PropTypes.string.isRequired,
+    // Click callback
+    onClick: PropTypes.func.isRequired,
+  },
+
+  getInitialState: function () {
+    return {
+      collapsed: this.props.collapsed,
+    };
+  },
+
+  // Events
+
+  onClick: function (event) {
+    this.props.onClick(event);
+  },
+
+  // Rendering
+
+  render: function () {
+    let title = this.state.collapsed ?
+      this.props.expandPaneTitle :
+      this.props.collapsePaneTitle;
+
+    let classNames = ["devtools-button", "sidebar-toggle"];
+    if (this.state.collapsed) {
+      classNames.push("pane-collapsed");
+    }
+
+    return (
+      button({
+        className: classNames.join(" "),
+        title: title,
+        onClick: this.onClick
+      })
+    );
+  }
+});
+
+module.exports = SidebarToggle;
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -1,28 +1,29 @@
 [DEFAULT]
 support-files =
   head.js
 
+[test_frame_01.html]
 [test_HSplitBox_01.html]
 [test_notification_box_01.html]
 [test_notification_box_02.html]
 [test_notification_box_03.html]
 [test_reps_attribute.html]
 [test_reps_date-time.html]
 [test_reps_function.html]
 [test_reps_grip.html]
 [test_reps_null.html]
 [test_reps_number.html]
 [test_reps_object-with-text.html]
 [test_reps_object-with-url.html]
 [test_reps_stylesheet.html]
 [test_reps_undefined.html]
 [test_reps_window.html]
-[test_frame_01.html]
+[test_sidebar_toggle.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
 [test_tree_07.html]
 [test_tree_08.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test sidebar toggle button
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Sidebar toggle button test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+  let SidebarToggle = browserRequire("devtools/client/shared/components/sidebar-toggle.js");
+
+  try {
+    yield test();
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+
+  function test() {
+    const output1 = shallowRenderComponent(SidebarToggle, {
+      collapsed: false,
+      collapsePaneTitle: "Expand",
+      expandPaneTitle: "Collapse"
+    });
+
+    is(output1.type, "button", "Output is a button element");
+    is(output1.props.title, "Expand", "Proper title is set");
+    is(output1.props.className.indexOf("pane-collapsed"), -1,
+      "Proper class name is set");
+
+    const output2 = shallowRenderComponent(SidebarToggle, {
+      collapsed: true,
+      collapsePaneTitle: "Expand",
+      expandPaneTitle: "Collapse"
+    });
+
+    is(output2.props.title, "Collapse", "Proper title is set");
+    ok(output2.props.className.indexOf("pane-collapsed") >= 0,
+      "Proper class name is set");
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/shared/widgets/view-helpers.js
+++ b/devtools/client/shared/widgets/view-helpers.js
@@ -239,17 +239,17 @@ const ViewHelpers = exports.ViewHelpers 
 
     // Hiding is always handled via margins, not the hidden attribute.
     pane.removeAttribute("hidden");
 
     // Add a class to the pane to handle min-widths, margins and animations.
     pane.classList.add("generic-toggled-pane");
 
     // Avoid useless toggles.
-    if (flags.visible == !pane.hasAttribute("pane-collapsed")) {
+    if (flags.visible == !pane.classList.contains("pane-collapsed")) {
       if (flags.callback) {
         flags.callback();
       }
       return;
     }
 
     // The "animated" attributes enables animated toggles (slide in-out).
     if (flags.animated) {
@@ -262,24 +262,24 @@ const ViewHelpers = exports.ViewHelpers 
     let doToggle = () => {
       // Negative margins are applied to "right" and "left" to support RTL and
       // LTR directions, as well as to "bottom" to support vertical layouts.
       // Unnecessary negative margins are forced to 0 via CSS in widgets.css.
       if (flags.visible) {
         pane.style.marginLeft = "0";
         pane.style.marginRight = "0";
         pane.style.marginBottom = "0";
-        pane.removeAttribute("pane-collapsed");
+        pane.classList.remove("pane-collapsed");
       } else {
         let width = Math.floor(pane.getAttribute("width")) + 1;
         let height = Math.floor(pane.getAttribute("height")) + 1;
         pane.style.marginLeft = -width + "px";
         pane.style.marginRight = -width + "px";
         pane.style.marginBottom = -height + "px";
-        pane.setAttribute("pane-collapsed", "");
+        pane.classList.add("pane-collapsed");
       }
 
       // Wait for the animation to end before calling afterToggle()
       if (flags.animated) {
         pane.addEventListener("transitionend", function onEvent() {
           pane.removeEventListener("transitionend", onEvent, false);
           // Prevent unwanted transitions: if the panel is hidden and the layout
           // changes margins will be updated and the panel will pop out.
--- a/devtools/client/themes/debugger.css
+++ b/devtools/client/themes/debugger.css
@@ -638,17 +638,17 @@
 .theme-firebug #step-out {
   list-style-image: url(images/firebug/debugger-step-out.svg);
 }
 
 #instruments-pane-toggle {
   list-style-image: var(--theme-pane-collapse-image);
 }
 
-#instruments-pane-toggle[pane-collapsed] {
+#instruments-pane-toggle.pane-collapsed {
   list-style-image: var(--theme-pane-expand-image);
 }
 
 /* Horizontal vs. vertical layout */
 
 #vertical-layout-panes-container {
   min-height: 35vh;
   max-height: 80vh;
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -73,34 +73,16 @@
 }
 
 #inspector-breadcrumbs .breadcrumbs-widget-item {
   white-space: nowrap;
   flex-shrink: 0;
   font: message-box;
 }
 
-/* Expand/collapse panel toolbar button */
-
-#inspector-pane-toggle::before {
-  background-image: var(--theme-pane-collapse-image);
-}
-
-#inspector-pane-toggle[pane-collapsed]::before {
-  background-image: var(--theme-pane-expand-image);
-}
-
-/* Rotate button icon 90deg if the toolbox container is
-  in vertical mode (sidebar displayed under the main panel) */
-@media (max-width: 700px) {
-  #inspector-pane-toggle::before {
-    transform: rotate(90deg);
-  }
-}
-
 /* Add element toolbar button */
 
 #inspector-element-add-button::before {
   background-image: url("chrome://devtools/skin/images/add.svg");
 }
 
 /* "no results" warning message displayed in the ruleview and in the computed view */
 
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -505,17 +505,17 @@
 }
 
 /* Network request details */
 
 #details-pane-toggle {
   list-style-image: var(--theme-pane-collapse-image);
 }
 
-#details-pane-toggle[pane-collapsed] {
+#details-pane-toggle.pane-collapsed {
   list-style-image: var(--theme-pane-expand-image);
 }
 
 /* Network request details tabpanels */
 
 .tabpanel-content {
   background-color: var(--theme-sidebar-background);
 }
--- a/devtools/client/themes/webaudioeditor.css
+++ b/devtools/client/themes/webaudioeditor.css
@@ -144,17 +144,17 @@ text {
   list-style-image: var(--theme-pane-collapse-image);
 }
 
 #inspector-pane-toggle > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
-#inspector-pane-toggle[pane-collapsed] {
+#inspector-pane-toggle.pane-collapsed {
   list-style-image: var(--theme-pane-expand-image);
 }
 
 /**
  * Automation Styles
  */
 
 #automation-param-toolbar .automation-param-button[selected] {
--- a/devtools/client/themes/widgets.css
+++ b/devtools/client/themes/widgets.css
@@ -92,17 +92,17 @@
     border-inline-start-width: 0;
     margin-inline-end: 0;
     margin-inline-start: 0;
 
     /* In some edge case the cursor is not changed to n-resize */
     cursor: n-resize;
   }
 
-  .devtools-responsive-container > .devtools-sidebar-tabs:not([pane-collapsed]) {
+  .devtools-responsive-container > .devtools-sidebar-tabs:not(.pane-collapsed) {
     /* When the panel is collapsed min/max height should not be applied because
        collapsing relies on negative margins, which implies constant height. */
     min-height: 35vh;
     max-height: 75vh;
   }
 
   .devtools-responsive-container .generic-toggled-pane {
     /* To hide generic-toggled-pane, negative margins are applied dynamically.
--- a/devtools/client/webaudioeditor/views/utils.js
+++ b/devtools/client/webaudioeditor/views/utils.js
@@ -60,17 +60,17 @@ var ToggleMixin = {
     this._viewController({ visible: false, delayed: false, animated: false });
   },
 
   /**
    * Returns a boolean indicating whether or not the view.
    * is currently being shown.
    */
   isVisible: function () {
-    return !this.el.hasAttribute("pane-collapsed");
+    return !this.el.classList.contains("pane-collapsed");
   },
 
   /**
    * Toggles the visibility of the view.
    *
    * @param object visible
    *        - visible: boolean indicating whether the panel should be shown or not
    *        - animated: boolean indiciating whether the pane should be animated
@@ -83,21 +83,21 @@ var ToggleMixin = {
       animated: animated != null ? animated : !!this._animated,
       delayed: delayed != null ? delayed : !!this._delayed,
       callback: () => window.emit(this._toggleEvent, visible)
     };
 
     ViewHelpers.togglePane(flags, this.el);
 
     if (flags.visible) {
-      this.button.removeAttribute("pane-collapsed");
+      this.button.classList.remove("pane-collapsed");
       this.button.setAttribute("tooltiptext", this._collapseString);
     }
     else {
-      this.button.setAttribute("pane-collapsed", "");
+      this.button.classList.add("pane-collapsed");
       this.button.setAttribute("tooltiptext", this._expandString);
     }
   },
 
   _onToggle: function () {
     this._viewController({ visible: !this.isVisible() });
   }
 };
--- a/layout/tools/reftest/mach_commands.py
+++ b/layout/tools/reftest/mach_commands.py
@@ -224,16 +224,23 @@ class ReftestRunner(MozbuildObject):
         }
 
         if not kwargs["tests"]:
             kwargs["tests"] = [os.path.join(*default_manifest[kwargs["suite"]])]
 
         kwargs["extraProfileFiles"].append(
             os.path.join(self.topsrcdir, "mobile", "android", "fonts"))
 
+        hyphenation_path = os.path.join(self.topsrcdir, "intl", "locales")
+
+        for (dirpath, dirnames, filenames) in os.walk(hyphenation_path):
+            for filename in filenames:
+                if filename.endswith('.dic'):
+                    kwargs["extraProfileFiles"].append(os.path.join(dirpath, filename))
+
         if not kwargs["httpdPath"]:
             kwargs["httpdPath"] = os.path.join(self.tests_dir, "modules")
         if not kwargs["symbolsPath"]:
             kwargs["symbolsPath"] = os.path.join(self.topobjdir, "crashreporter-symbols")
         if not kwargs["xrePath"]:
             kwargs["xrePath"] = os.environ.get("MOZ_HOST_BIN")
         if not kwargs["app"]:
             kwargs["app"] = self.substs["ANDROID_PACKAGE_NAME"]
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -684,22 +684,26 @@ class RefTest(object):
             )
         finally:
             self.cleanup(profileDir)
         return status
 
     def copyExtraFilesToProfile(self, options, profile):
         "Copy extra files or dirs specified on the command line to the testing profile."
         profileDir = profile.profile
+        if not os.path.exists(os.path.join(profileDir, "hyphenation")):
+            os.makedirs(os.path.join(profileDir, "hyphenation"))
         for f in options.extraProfileFiles:
             abspath = self.getFullPath(f)
             if os.path.isfile(abspath):
                 if os.path.basename(abspath) == 'user.js':
                     extra_prefs = mozprofile.Preferences.read_prefs(abspath)
                     profile.set_preferences(extra_prefs)
+                elif os.path.basename(abspath).endswith('.dic'):
+                    shutil.copy2(abspath, os.path.join(profileDir, "hyphenation"))
                 else:
                     shutil.copy2(abspath, profileDir)
             elif os.path.isdir(abspath):
                 dest = os.path.join(profileDir, os.path.basename(abspath))
                 shutil.copytree(abspath, dest)
             else:
                 self.log.warning(
                     "runreftest.py | Failed to copy %s to profile" % abspath)
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -1080,32 +1080,36 @@ Sync11Service.prototype = {
       // If we got a 401, we do not want to create a new meta/global - we
       // should be able to get the existing meta after we get a new node.
       if (this.recordManager.response.status == 401) {
         this._log.debug("Fetching meta/global record on the server returned 401.");
         this.errorHandler.checkServerError(this.recordManager.response);
         return false;
       }
 
-      if (!this.recordManager.response.success || !newMeta) {
+      if (this.recordManager.response.status == 404) {
         this._log.debug("No meta/global record on the server. Creating one.");
         newMeta = new WBORecord("meta", "global");
         newMeta.payload.syncID = this.syncID;
         newMeta.payload.storageVersion = STORAGE_VERSION;
         newMeta.payload.declined = this.engineManager.getDeclined();
 
         newMeta.isNew = true;
 
         this.recordManager.set(this.metaURL, newMeta);
         let uploadRes = newMeta.upload(this.resource(this.metaURL));
         if (!uploadRes.success) {
           this._log.warn("Unable to upload new meta/global. Failing remote setup.");
           this.errorHandler.checkServerError(uploadRes);
           return false;
         }
+      } else if (!newMeta) {
+        this._log.warn("Unable to get meta/global. Failing remote setup.");
+        this.errorHandler.checkServerError(this.recordManager.response);
+        return false;
       } else {
         // If newMeta, then it stands to reason that meta != null.
         newMeta.isNew   = meta.isNew;
         newMeta.changed = meta.changed;
       }
 
       // Switch in the new meta object and record the new time.
       meta              = newMeta;
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -48,25 +48,37 @@ function run_test() {
 
     let ts = new_timestamp();
     collectionsHelper.update_collection("crypto", ts);
     collectionsHelper.update_collection("clients", ts);
     collectionsHelper.update_collection("meta", ts);
     return_timestamp(request, response, ts);
   }
 
-  let server = httpd_setup({
+  const GLOBAL_PATH = "/1.1/johndoe/storage/meta/global";
+  const INFO_PATH = "/1.1/johndoe/info/collections";
+
+  let handlers = {
     "/1.1/johndoe/storage": storageHandler,
     "/1.1/johndoe/storage/crypto/keys": upd("crypto", keysWBO.handler()),
     "/1.1/johndoe/storage/crypto": upd("crypto", cryptoColl.handler()),
     "/1.1/johndoe/storage/clients": upd("clients", clients.handler()),
+    "/1.1/johndoe/storage/meta": upd("meta", wasCalledHandler(metaColl)),
     "/1.1/johndoe/storage/meta/global": upd("meta", wasCalledHandler(meta_global)),
-    "/1.1/johndoe/storage/meta": upd("meta", wasCalledHandler(metaColl)),
     "/1.1/johndoe/info/collections": collectionsHelper.handler
-  });
+  };
+
+  function mockHandler(path, mock) {
+    server.registerPathHandler(path, mock(handlers[path]));
+    return {
+      restore() { server.registerPathHandler(path, handlers[path]); }
+    }
+  }
+
+  let server = httpd_setup(handlers);
 
   try {
     _("Log in.");
     ensureLegacyIdentityManager();
     Service.serverURL = server.baseURI;
 
     _("Checking Status.sync with no credentials.");
     Service.verifyAndFetchSymmetricKeys();
@@ -84,16 +96,73 @@ function run_test() {
     Service.serverURL = server.baseURI;
     Service.login("johndoe", "ilovejane", syncKey);
     do_check_true(Service.isLoggedIn);
 
     _("Checking that remoteSetup returns true when credentials have changed.");
     Service.recordManager.get(Service.metaURL).payload.syncID = "foobar";
     do_check_true(Service._remoteSetup());
 
+    let returnStatusCode = (method, code) => (oldMethod) => (req, res) => {
+      if (req.method === method) {
+        res.setStatusLine(req.httpVersion, code, "");
+      } else {
+        oldMethod(req, res);
+      }
+    };
+
+    let mock = mockHandler(GLOBAL_PATH, returnStatusCode("GET", 401));
+    Service.recordManager.del(Service.metaURL);
+    _("Checking that remoteSetup returns false on 401 on first get /meta/global.");
+    do_check_false(Service._remoteSetup());
+    mock.restore();
+
+    Service.login("johndoe", "ilovejane", syncKey);
+    mock = mockHandler(GLOBAL_PATH, returnStatusCode("GET", 503));
+    Service.recordManager.del(Service.metaURL);
+    _("Checking that remoteSetup returns false on 503 on first get /meta/global.");
+    do_check_false(Service._remoteSetup());
+    do_check_eq(Service.status.sync, METARECORD_DOWNLOAD_FAIL);
+    mock.restore();
+
+    mock = mockHandler(GLOBAL_PATH, returnStatusCode("GET", 404));
+    Service.recordManager.del(Service.metaURL);
+    _("Checking that remoteSetup recovers on 404 on first get /meta/global.");
+    do_check_true(Service._remoteSetup());
+    mock.restore();
+
+    let makeOutdatedMeta = () => {
+      Service.metaModified = 0;
+      let infoResponse = Service._fetchInfo();
+      return {
+        status: infoResponse.status,
+        obj: {
+          crypto: infoResponse.obj.crypto,
+          clients: infoResponse.obj.clients,
+          meta: 1
+        }
+      };
+    }
+
+    _("Checking that remoteSetup recovers on 404 on get /meta/global after clear cached one.");
+    mock = mockHandler(GLOBAL_PATH, returnStatusCode("GET", 404));
+    Service.recordManager.set(Service.metaURL, { isNew: false });
+    do_check_true(Service._remoteSetup(makeOutdatedMeta()));
+    mock.restore();
+
+    _("Checking that remoteSetup returns false on 503 on get /meta/global after clear cached one.");
+    mock = mockHandler(GLOBAL_PATH, returnStatusCode("GET", 503));
+    Service.status.sync = "";
+    Service.recordManager.set(Service.metaURL, { isNew: false });
+    do_check_false(Service._remoteSetup(makeOutdatedMeta()));
+    do_check_eq(Service.status.sync, "");
+    mock.restore();
+
+    metaColl.delete({});
+
     _("Do an initial sync.");
     let beforeSync = Date.now()/1000;
     Service.sync();
 
     _("Checking that remoteSetup returns true.");
     do_check_true(Service._remoteSetup());
 
     _("Verify that the meta record was uploaded.");
--- a/testing/mozharness/configs/android/androidarm.py
+++ b/testing/mozharness/configs/android/androidarm.py
@@ -69,16 +69,17 @@ config = {
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s",
                 "--symbols-path=%(symbols_path)s",
                 "--quiet",
                 "--log-raw=%(raw_log_file)s",
                 "--log-errorsummary=%(error_summary_file)s",
                 "--extra-profile-file=fonts",
+                "--extra-profile-file=hyphenation",
                 "--screenshot-on-fail",
             ],
         },
         "mochitest-gl": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
             "options": [
                 "--dm_trans=sut",
@@ -159,16 +160,17 @@ config = {
                 "--devicePort=%(device_port)s",
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--httpd-path",
                 "%(modules_dir)s",
                 "--symbols-path=%(symbols_path)s",
                 "--total-chunks=16",
                 "--extra-profile-file=fonts",
+                "--extra-profile-file=hyphenation",
                 "--suite=reftest",
                 "--log-raw=%(raw_log_file)s",
                 "--log-errorsummary=%(error_summary_file)s",
             ],
             "tests": ["tests/layout/reftests/reftest.list"],
         },
         "crashtest": {
             "run_filename": "remotereftest.py",
--- a/testing/mozharness/configs/android/androidarm_4_3.py
+++ b/testing/mozharness/configs/android/androidarm_4_3.py
@@ -73,16 +73,17 @@ config = {
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s",
                 "--symbols-path=%(symbols_path)s",
                 "--quiet",
                 "--log-raw=%(raw_log_file)s",
                 "--log-errorsummary=%(error_summary_file)s",
                 "--extra-profile-file=fonts",
+                "--extra-profile-file=hyphenation",
                 "--screenshot-on-fail",
             ],
         },
         "mochitest-gl": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
             "options": [
                 "--dm_trans=adb",
@@ -196,16 +197,17 @@ config = {
                 "--xre-path=%(xre_path)s",
                 "--utility-path=%(utility_path)s",
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--httpd-path", "%(modules_dir)s",
                 "--symbols-path=%(symbols_path)s",
                 "--total-chunks=16",
                 "--extra-profile-file=fonts",
+                "--extra-profile-file=hyphenation",
                 "--suite=reftest",
                 "--log-raw=%(raw_log_file)s",
                 "--log-errorsummary=%(error_summary_file)s",
             ],
             "tests": ["tests/layout/reftests/reftest.list",],
         },
         "reftest-debug": {
             "run_filename": "remotereftest.py",
@@ -218,16 +220,17 @@ config = {
                 "--xre-path=%(xre_path)s",
                 "--utility-path=%(utility_path)s",
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--httpd-path", "%(modules_dir)s",
                 "--symbols-path=%(symbols_path)s",
                 "--total-chunks=48",
                 "--extra-profile-file=fonts",
+                "--extra-profile-file=hyphenation",
                 "tests/layout/reftests/reftest.list",
             ],
         },
         "crashtest": {
             "run_filename": "remotereftest.py",
             "testsdir": "reftest",
             "options": [
                 "--app=%(app)s",