Bug 1520957 - [release 119] Add 'Collapse all' and 'Expand all' sources context menu options (#7714). r=dwalsh
☠☠ backed out by 5b1c54cbac38 ☠ ☠
authorGary Blackwood <gary@garyblackwood.co.uk>
Fri, 18 Jan 2019 12:10:32 -0500
changeset 514559 118720bcf7bd6cece9570eb8b459bc7d57cc227f
parent 514558 6a62f14cbf219e30bb546410fda9c540307bb14d
child 514560 f1f01efee7672d83fc3940fa3785ebc7a4d68307
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdwalsh
bugs1520957
milestone66.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
Bug 1520957 - [release 119] Add 'Collapse all' and 'Expand all' sources context menu options (#7714). r=dwalsh
devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
devtools/client/debugger/new/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js
devtools/client/debugger/new/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
devtools/client/locales/en-US/debugger.properties
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
@@ -76,16 +76,18 @@ type Props = {
 type State = {
   parentMap: ParentMap,
   sourceTree: TreeDirectory,
   uncollapsedTree: TreeDirectory,
   listItems?: any,
   highlightItems?: any
 };
 
+type SetExpanded = (item: TreeNode, expanded: boolean, altKey: boolean) => void;
+
 class SourcesTree extends Component<Props, State> {
   mounted: boolean;
 
   constructor(props: Props) {
     super(props);
     const { debuggeeUrl, sources, projectRoot } = this.props;
 
     this.state = createTree({
@@ -233,31 +235,33 @@ class SourcesTree extends Component<Prop
     return sourceTree.contents;
   };
 
   renderItem = (
     item: TreeNode,
     depth: number,
     focused: boolean,
     _,
-    expanded: boolean
+    expanded: boolean,
+    { setExpanded }: { setExpanded: SetExpanded }
   ) => {
     const { debuggeeUrl, projectRoot } = this.props;
 
     return (
       <SourcesTreeItem
         item={item}
         depth={depth}
         focused={focused}
         expanded={expanded}
         focusItem={this.onFocus}
         selectItem={this.selectItem}
         source={this.getSource(item)}
         debuggeeUrl={debuggeeUrl}
         projectRoot={projectRoot}
+        setExpanded={setExpanded}
       />
     );
   };
 
   renderTree() {
     const { expanded, focused } = this.props;
     const { highlightItems, listItems, parentMap } = this.state;
 
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
@@ -37,22 +37,32 @@ type Props = {
   item: TreeNode,
   depth: number,
   focused: boolean,
   expanded: boolean,
   hasMatchingGeneratedSource: boolean,
   hasSiblingOfSameName: boolean,
   focusItem: TreeNode => void,
   selectItem: TreeNode => void,
+  setExpanded: (TreeNode, boolean, boolean) => void,
   clearProjectDirectoryRoot: typeof actions.clearProjectDirectoryRoot,
   setProjectDirectoryRoot: typeof actions.setProjectDirectoryRoot
 };
 
 type State = {};
 
+type MenuOption = {
+  id: string,
+  label: string,
+  disabled: boolean,
+  click: () => any
+};
+
+type ContextMenu = Array<MenuOption>;
+
 class SourceTreeItem extends Component<Props, State> {
   getIcon(item: TreeNode, depth: number) {
     const { debuggeeUrl, projectRoot, source } = this.props;
 
     if (item.path === "webpack://") {
       return <Svg name="webpack" />;
     } else if (item.path === "ng://") {
       return <Svg name="angular" />;
@@ -115,41 +125,63 @@ class SourceTreeItem extends Component<P
           disabled: false,
           click: () => copyToTheClipboard(contents.url)
         };
 
         menuOptions.push(copySourceUri2);
       }
     }
 
-    if (isDirectory(item) && features.root) {
-      const { path } = item;
-      const { projectRoot } = this.props;
+    if (isDirectory(item)) {
+      this.addCollapseExpandAllOptions(menuOptions, item);
+
+      if (features.root) {
+        const { path } = item;
+        const { projectRoot } = this.props;
 
-      if (projectRoot.endsWith(path)) {
-        menuOptions.push({
-          id: "node-remove-directory-root",
-          label: removeDirectoryRootLabel,
-          disabled: false,
-          click: () => this.props.clearProjectDirectoryRoot()
-        });
-      } else {
-        menuOptions.push({
-          id: "node-set-directory-root",
-          label: setDirectoryRootLabel,
-          accesskey: setDirectoryRootKey,
-          disabled: false,
-          click: () => this.props.setProjectDirectoryRoot(path)
-        });
+        if (projectRoot.endsWith(path)) {
+          menuOptions.push({
+            id: "node-remove-directory-root",
+            label: removeDirectoryRootLabel,
+            disabled: false,
+            click: () => this.props.clearProjectDirectoryRoot()
+          });
+        } else {
+          menuOptions.push({
+            id: "node-set-directory-root",
+            label: setDirectoryRootLabel,
+            accesskey: setDirectoryRootKey,
+            disabled: false,
+            click: () => this.props.setProjectDirectoryRoot(path)
+          });
+        }
       }
     }
 
     showMenu(event, menuOptions);
   };
 
+  addCollapseExpandAllOptions = (menuOptions: ContextMenu, item: TreeNode) => {
+    const { setExpanded } = this.props;
+
+    menuOptions.push({
+      id: "node-menu-collapse-all",
+      label: L10N.getStr("collapseAll.label"),
+      disabled: false,
+      click: () => setExpanded(item, false, true)
+    });
+
+    menuOptions.push({
+      id: "node-menu-expand-all",
+      label: L10N.getStr("expandAll.label"),
+      disabled: false,
+      click: () => setExpanded(item, true, true)
+    });
+  };
+
   renderItemArrow() {
     const { item, expanded } = this.props;
     return isDirectory(item) ? (
       <AccessibleImage className={classnames("arrow", { expanded })} />
     ) : (
       <i className="no-arrow" />
     );
   }
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js
@@ -30,16 +30,28 @@ describe("SourceTreeItem", () => {
     component.simulate("contextmenu", event);
     expect(instance.onContextMenu).toHaveBeenCalledWith(event, item);
   });
 
   describe("onContextMenu of the tree", () => {
     it("shows context menu on directory to set as root", async () => {
       const menuOptions = [
         {
+          click: expect.any(Function),
+          disabled: false,
+          id: "node-menu-collapse-all",
+          label: "Collapse all"
+        },
+        {
+          click: expect.any(Function),
+          disabled: false,
+          id: "node-menu-expand-all",
+          label: "Expand all"
+        },
+        {
           accesskey: "r",
           click: expect.any(Function),
           disabled: false,
           id: "node-set-directory-root",
           label: "Set directory root"
         }
       ];
       const mockEvent = {
@@ -50,17 +62,17 @@ describe("SourceTreeItem", () => {
         projectRoot: "root/"
       });
       await instance.onContextMenu(mockEvent, createMockDirectory());
       expect(showMenu).toHaveBeenCalledWith(mockEvent, menuOptions);
 
       expect(mockEvent.preventDefault).toHaveBeenCalled();
       expect(mockEvent.stopPropagation).toHaveBeenCalled();
 
-      showMenu.mock.calls[0][1][0].click();
+      showMenu.mock.calls[0][1][2].click();
       expect(props.setProjectDirectoryRoot).toHaveBeenCalled();
       expect(props.clearProjectDirectoryRoot).not.toHaveBeenCalled();
       expect(copyToTheClipboard).not.toHaveBeenCalled();
     });
 
     it("shows context menu on file to copy source uri", async () => {
       const menuOptions = [
         {
@@ -93,16 +105,28 @@ describe("SourceTreeItem", () => {
       expect(copyToTheClipboard).toHaveBeenCalled();
     });
 
     it("shows context menu on root to remove directory root", async () => {
       const menuOptions = [
         {
           click: expect.any(Function),
           disabled: false,
+          id: "node-menu-collapse-all",
+          label: "Collapse all"
+        },
+        {
+          click: expect.any(Function),
+          disabled: false,
+          id: "node-menu-expand-all",
+          label: "Expand all"
+        },
+        {
+          click: expect.any(Function),
+          disabled: false,
           id: "node-remove-directory-root",
           label: "Remove directory root"
         }
       ];
       const { props, instance } = render({
         projectRoot: "root/"
       });
 
@@ -116,17 +140,17 @@ describe("SourceTreeItem", () => {
         createMockDirectory("root/", "root")
       );
 
       expect(showMenu).toHaveBeenCalledWith(mockEvent, menuOptions);
 
       expect(mockEvent.preventDefault).toHaveBeenCalled();
       expect(mockEvent.stopPropagation).toHaveBeenCalled();
 
-      showMenu.mock.calls[0][1][0].click();
+      showMenu.mock.calls[0][1][2].click();
       expect(props.setProjectDirectoryRoot).not.toHaveBeenCalled();
       expect(props.clearProjectDirectoryRoot).toHaveBeenCalled();
       expect(copyToTheClipboard).not.toHaveBeenCalled();
     });
   });
 
   describe("renderItem", () => {
     it("should show icon for webpack item", async () => {
@@ -285,16 +309,17 @@ function generateDefaults(overrides) {
     item,
     source,
     debuggeeUrl: "http://mdn.com",
     projectRoot: "",
     clearProjectDirectoryRoot: jest.fn(),
     setProjectDirectoryRoot: jest.fn(),
     selectItem: jest.fn(),
     focusItem: jest.fn(),
+    setExpanded: jest.fn(),
     ...overrides
   };
 }
 
 function render(overrides = {}) {
   const props = generateDefaults(overrides);
   const component = shallow(<SourcesTreeItem.WrappedComponent {...props} />);
   const defaultState = component.state();
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap
@@ -39,16 +39,17 @@ Object {
         className="suffix"
       >
         (mapped)
       </span>
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "expanded": false,
       "focusItem": [MockFunction],
@@ -67,16 +68,17 @@ Object {
           "url": undefined,
         },
         "name": "one.js",
         "path": "http://mdn.com/one.js",
         "type": "source",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -114,16 +116,17 @@ Object {
               },
               "name": "one.js",
               "path": "http://mdn.com/one.js",
               "type": "source",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -200,16 +203,17 @@ Object {
         "url": undefined,
       },
       "name": "one.js",
       "path": "http://mdn.com/one.js",
       "type": "source",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -241,16 +245,17 @@ Object {
     >
        
       root
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 0,
       "expanded": false,
@@ -269,16 +274,17 @@ Object {
           "url": undefined,
         },
         "name": "root",
         "path": "root",
         "type": "source",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -316,16 +322,17 @@ Object {
               },
               "name": "root",
               "path": "root",
               "type": "source",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -384,16 +391,17 @@ Object {
         "url": undefined,
       },
       "name": "root",
       "path": "root",
       "type": "source",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -425,16 +433,17 @@ Object {
     >
        
       http://mdn.com
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 0,
       "expanded": false,
@@ -442,16 +451,17 @@ Object {
       "item": Object {
         "contents": Array [],
         "name": "http://mdn.com",
         "path": "root",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -478,16 +488,17 @@ Object {
               "contents": Array [],
               "name": "http://mdn.com",
               "path": "root",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -535,16 +546,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "http://mdn.com",
       "path": "root",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -576,16 +588,17 @@ Object {
     >
        
       http://mdn.com
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 0,
       "expanded": false,
@@ -594,16 +607,17 @@ Object {
       "item": Object {
         "contents": Array [],
         "name": "http://mdn.com",
         "path": "root",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -631,16 +645,17 @@ Object {
               "contents": Array [],
               "name": "http://mdn.com",
               "path": "root",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -689,16 +704,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "http://mdn.com",
       "path": "root",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -730,16 +746,17 @@ Object {
     >
        
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 1,
       "expanded": true,
@@ -748,16 +765,17 @@ Object {
       "item": Object {
         "contents": Array [],
         "name": "folder",
         "path": "folder/",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": null,
     },
     "refs": Object {},
     "state": null,
     "updater": Updater {
       "_callbacks": Array [],
       "_renderer": ReactShallowRenderer {
@@ -774,16 +792,17 @@ Object {
               "contents": Array [],
               "name": "folder",
               "path": "folder/",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={null}
         />,
         "_forcedUpdate": false,
         "_instance": [Circular],
         "_newState": null,
         "_rendered": <div
           className="node focused"
@@ -819,16 +838,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "folder",
       "path": "folder/",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": null,
   },
 }
 `;
 
 exports[`SourceTreeItem renderItem should show icon for angular item 1`] = `
 Object {
@@ -849,32 +869,34 @@ Object {
     >
        
       Angular
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "expanded": false,
       "focusItem": [MockFunction],
       "item": Object {
         "contents": Array [],
         "name": "ng://",
         "path": "ng://",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -900,16 +922,17 @@ Object {
               "contents": Array [],
               "name": "ng://",
               "path": "ng://",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -956,16 +979,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "ng://",
       "path": "ng://",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -997,32 +1021,34 @@ Object {
     >
        
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "expanded": false,
       "focusItem": [MockFunction],
       "item": Object {
         "contents": Array [],
         "name": "folder",
         "path": "folder/",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": null,
     },
     "refs": Object {},
     "state": null,
     "updater": Updater {
       "_callbacks": Array [],
       "_renderer": ReactShallowRenderer {
@@ -1037,16 +1063,17 @@ Object {
               "contents": Array [],
               "name": "folder",
               "path": "folder/",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={null}
         />,
         "_forcedUpdate": false,
         "_instance": [Circular],
         "_newState": null,
         "_rendered": <div
           className="node"
@@ -1080,16 +1107,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "folder",
       "path": "folder/",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": null,
   },
 }
 `;
 
 exports[`SourceTreeItem renderItem should show icon for folder with expanded arrow 1`] = `
 Object {
@@ -1110,16 +1138,17 @@ Object {
     >
        
       folder
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 1,
       "expanded": true,
@@ -1128,16 +1157,17 @@ Object {
       "item": Object {
         "contents": Array [],
         "name": "folder",
         "path": "folder/",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": null,
     },
     "refs": Object {},
     "state": null,
     "updater": Updater {
       "_callbacks": Array [],
       "_renderer": ReactShallowRenderer {
@@ -1154,16 +1184,17 @@ Object {
               "contents": Array [],
               "name": "folder",
               "path": "folder/",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={null}
         />,
         "_forcedUpdate": false,
         "_instance": [Circular],
         "_newState": null,
         "_rendered": <div
           className="node"
@@ -1199,16 +1230,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "folder",
       "path": "folder/",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": null,
   },
 }
 `;
 
 exports[`SourceTreeItem renderItem should show icon for moz-extension item 1`] = `
 Object {
@@ -1229,16 +1261,17 @@ Object {
     >
        
       moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 0,
       "expanded": false,
@@ -1246,16 +1279,17 @@ Object {
       "item": Object {
         "contents": Array [],
         "name": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856",
         "path": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -1282,16 +1316,17 @@ Object {
               "contents": Array [],
               "name": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856",
               "path": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -1339,16 +1374,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856",
       "path": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -1380,32 +1416,34 @@ Object {
     >
        
       Webpack
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "expanded": false,
       "focusItem": [MockFunction],
       "item": Object {
         "contents": Array [],
         "name": "webpack://",
         "path": "webpack://",
         "type": "directory",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -1431,16 +1469,17 @@ Object {
               "contents": Array [],
               "name": "webpack://",
               "path": "webpack://",
               "type": "directory",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -1487,16 +1526,17 @@ Object {
     "item": Object {
       "contents": Array [],
       "name": "webpack://",
       "path": "webpack://",
       "type": "directory",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -1541,16 +1581,17 @@ Object {
     >
        
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "expanded": false,
       "focusItem": [MockFunction],
@@ -1568,16 +1609,17 @@ Object {
           "url": undefined,
         },
         "name": "one.js",
         "path": "http://mdn.com/one.js",
         "type": "source",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -1614,16 +1656,17 @@ Object {
               },
               "name": "one.js",
               "path": "http://mdn.com/one.js",
               "type": "source",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -1694,16 +1737,17 @@ Object {
         "url": undefined,
       },
       "name": "one.js",
       "path": "http://mdn.com/one.js",
       "type": "source",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
@@ -1748,16 +1792,17 @@ Object {
     >
        
       one.js
        
     </span>
   </div>,
   "defaultState": null,
   "instance": SourceTreeItem {
+    "addCollapseExpandAllOptions": [Function],
     "context": Object {},
     "onClick": [Function],
     "onContextMenu": [Function],
     "props": Object {
       "clearProjectDirectoryRoot": [MockFunction],
       "debuggeeUrl": "http://mdn.com",
       "depth": 1,
       "expanded": false,
@@ -1776,16 +1821,17 @@ Object {
           "text": undefined,
           "url": "http://mdn.com/one.js",
         },
         "name": "one.js",
         "path": "mdn.com/one.js",
       },
       "projectRoot": "",
       "selectItem": [MockFunction],
+      "setExpanded": [MockFunction],
       "setProjectDirectoryRoot": [MockFunction],
       "source": Object {
         "contentType": "",
         "error": undefined,
         "id": "server1.conn13.child1/39",
         "isBlackBoxed": false,
         "isPrettyPrinted": false,
         "isWasm": false,
@@ -1823,16 +1869,17 @@ Object {
                 "url": "http://mdn.com/one.js",
               },
               "name": "one.js",
               "path": "mdn.com/one.js",
             }
           }
           projectRoot=""
           selectItem={[MockFunction]}
+          setExpanded={[MockFunction]}
           setProjectDirectoryRoot={[MockFunction]}
           source={
             Object {
               "contentType": "",
               "error": undefined,
               "id": "server1.conn13.child1/39",
               "isBlackBoxed": false,
               "isPrettyPrinted": false,
@@ -1904,16 +1951,17 @@ Object {
         "text": undefined,
         "url": "http://mdn.com/one.js",
       },
       "name": "one.js",
       "path": "mdn.com/one.js",
     },
     "projectRoot": "",
     "selectItem": [MockFunction],
+    "setExpanded": [MockFunction],
     "setProjectDirectoryRoot": [MockFunction],
     "source": Object {
       "contentType": "",
       "error": undefined,
       "id": "server1.conn13.child1/39",
       "isBlackBoxed": false,
       "isPrettyPrinted": false,
       "isWasm": false,
--- a/devtools/client/locales/en-US/debugger.properties
+++ b/devtools/client/locales/en-US/debugger.properties
@@ -25,16 +25,24 @@ copySource=Copy
 copySource.label=Copy source text
 copySource.accesskey=y
 
 # LOCALIZATION NOTE (copySourceUri2): This is the text that appears in the
 # context menu to copy the source URI of file open.
 copySourceUri2=Copy source URI
 copySourceUri2.accesskey=u
 
+# LOCALIZATION NOTE (collapseAll.label): This is the text that appears in the
+# context menu to collapse a directory and all of its subdirectories.
+collapseAll.label=Collapse all
+
+# LOCALIZATION NOTE (expandAll.label): This is the text that appears in the
+# context menu to expand a directory and all of its subdirectories.
+expandAll.label=Expand all
+
 # LOCALIZATION NOTE (setDirectoryRoot.label): This is the text that appears in the
 # context menu to set a directory as root directory
 setDirectoryRoot.label=Set directory root
 setDirectoryRoot.accesskey=r
 
 # LOCALIZATION NOTE (removeDirectoryRoot.label): This is the text that appears in the
 # context menu to remove a directory as root directory
 removeDirectoryRoot.label=Remove directory root