Bug 1527122 - [release 125] XHR breakpoints with methods (#7721). r=dwalsh
authorJaril <jarilvalenciano@gmail.com>
Mon, 11 Feb 2019 17:02:42 -0800
changeset 458688 1728a42ced0c
parent 458687 d73a4f61cf59
child 458689 2481b5afa32e
push id35544
push userccoroiu@mozilla.com
push dateTue, 12 Feb 2019 16:29:08 +0000
treeherdermozilla-central@c849fb69e2e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdwalsh
bugs1527122
milestone67.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 1527122 - [release 125] XHR breakpoints with methods (#7721). r=dwalsh
devtools/client/debugger/new/dist/debugger.css
devtools/client/debugger/new/src/components/SecondaryPanes/XHRBreakpoints.css
devtools/client/debugger/new/src/components/SecondaryPanes/XHRBreakpoints.js
devtools/client/debugger/new/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
devtools/client/debugger/new/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-breakpoints.js
devtools/client/debugger/new/test/mochitest/examples/fetch.js
--- a/devtools/client/debugger/new/dist/debugger.css
+++ b/devtools/client/debugger/new/dist/debugger.css
@@ -4232,57 +4232,67 @@ html[dir="rtl"] .command-bar {
   background: var(--theme-splitter-color);
   height: 10px;
   margin: 11px 6px 0 6px;
 }
 /* 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/>. */
 
-.xhr-input-form {
-  width: 100%;
-}
-
-.xhr-input {
-  width: 100%;
-  margin: 0;
-  border: 1px;
-  background-color: var(--theme-sidebar-background);
-  font-size: 12px;
-  padding: 0.5em 1.6em;
-  color: var(--theme-body-color);
-  outline: 0;
-}
-
-.xhr-input::placeholder {
-  font-style: italic;
-  color: var(--theme-comment);
-}
-
-.xhr-input:focus {
-  cursor: text;
-}
-
 .xhr-input-container {
-  display: flex;
+  display: block;
   border: 1px solid transparent;
 }
 
 .xhr-input-container.focused {
   border: 1px solid var(--theme-highlight-blue);
 }
 
 :root.theme-dark .xhr-input-container.focused {
   border: 1px solid var(--blue-50);
 }
 
 .xhr-input-container.error {
   border: 1px solid red;
 }
 
+.xhr-container label {
+  display: flex;
+}
+
+.xhr-input-form {
+  display: inline-flex;
+  width: 100%;
+  padding: 0.5em 1em 0.5em 1em;
+}
+
+.xhr-checkbox {
+  margin-inline-start: 0;
+}
+
+.xhr-input-url {
+  border: 1px;
+  padding: 0em 0.6em 0em 0.6em;
+  flex-grow: 1;
+  background-color: var(--theme-sidebar-background);
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--theme-body-color);
+}
+
+.xhr-input-url::placeholder {
+  font-style: italic;
+  color: var(--theme-comment);
+}
+
+.xhr-input-url:focus {
+  cursor: text;
+  outline: none;
+}
+
 .xhr-container {
   border-left: 4px solid transparent;
   width: 100%;
   color: var(--theme-body-color);
   padding: 0.25em 1em;
   background-color: var(--theme-body-background);
   display: flex;
   align-items: center;
@@ -4293,35 +4303,54 @@ html[dir="rtl"] .command-bar {
 :root.theme-light .xhr-container:hover {
   background-color: var(--theme-selection-background-hover);
 }
 
 :root.theme-dark .xhr-container:hover {
   background-color: var(--theme-selection-background-hover);
 }
 
-.xhr-checkbox {
-    margin-left: 0px;
-}
-
-.xhr-label {
+.xhr-label-method {
+  padding: 0px 2px 0px 2px;
+  line-height: 15px;
+  display: inline-block;
+}
+
+.xhr-input-method {
+  display: none;
+}
+
+.xhr-input-container.focused .xhr-input-method {
+  display: block;
+}
+
+.xhr-label-url {
   max-width: calc(100% - var(--breakpoint-expression-right-clear-space));
+  color: var(--theme-comment);
   display: inline-block;
   cursor: text;
   flex-grow: 1;
   text-overflow: ellipsis;
-  padding-inline-end: 8px;
+  overflow: hidden;
+  padding: 0px 2px 0px 2px;
+  line-height: 15px;
   font-size: 11px;
 }
 
-.xhr-container .close-btn {
+.xhr-container label {
+  flex-grow: 1;
+  display: flex;
+  padding-inline-end: 36px;
+  align-items: center;
+  overflow-x: hidden;
+}
+
+.xhr-container__close-btn {
   offset-inline-end: 12px;
-  inset-inline-end: 12px;
   offset-inline-start: auto;
-  inset-inline-start: auto;
   position: absolute;
   top: 8px;
 }
 /* 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/>. */
 
 .event-listeners-content {
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/XHRBreakpoints.css
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/XHRBreakpoints.css
@@ -1,53 +1,63 @@
 /* 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/>. */
 
-.xhr-input-form {
-  width: 100%;
-}
-
-.xhr-input {
-  width: 100%;
-  margin: 0;
-  border: 1px;
-  background-color: var(--theme-sidebar-background);
-  font-size: 12px;
-  padding: 0.5em 1.6em;
-  color: var(--theme-body-color);
-  outline: 0;
-}
-
-.xhr-input::placeholder {
-  font-style: italic;
-  color: var(--theme-comment);
-}
-
-.xhr-input:focus {
-  cursor: text;
-}
-
 .xhr-input-container {
-  display: flex;
+  display: block;
   border: 1px solid transparent;
 }
 
 .xhr-input-container.focused {
   border: 1px solid var(--theme-highlight-blue);
 }
 
 :root.theme-dark .xhr-input-container.focused {
   border: 1px solid var(--blue-50);
 }
 
 .xhr-input-container.error {
   border: 1px solid red;
 }
 
+.xhr-container label {
+  display: flex;
+}
+
+.xhr-input-form {
+  display: inline-flex;
+  width: 100%;
+  padding: 0.5em 1em 0.5em 1em;
+}
+
+.xhr-checkbox {
+  margin-inline-start: 0;
+}
+
+.xhr-input-url {
+  border: 1px;
+  padding: 0em 0.6em 0em 0.6em;
+  flex-grow: 1;
+  background-color: var(--theme-sidebar-background);
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--theme-body-color);
+}
+
+.xhr-input-url::placeholder {
+  font-style: italic;
+  color: var(--theme-comment);
+}
+
+.xhr-input-url:focus {
+  cursor: text;
+  outline: none;
+}
+
 .xhr-container {
   border-left: 4px solid transparent;
   width: 100%;
   color: var(--theme-body-color);
   padding: 0.25em 1em;
   background-color: var(--theme-body-background);
   display: flex;
   align-items: center;
@@ -58,30 +68,49 @@
 :root.theme-light .xhr-container:hover {
   background-color: var(--theme-selection-background-hover);
 }
 
 :root.theme-dark .xhr-container:hover {
   background-color: var(--theme-selection-background-hover);
 }
 
-.xhr-checkbox {
-    margin-left: 0px;
+.xhr-label-method {
+  padding: 0px 2px 0px 2px;
+  line-height: 15px;
+  display: inline-block;
 }
 
-.xhr-label {
+.xhr-input-method {
+  display: none;
+}
+
+.xhr-input-container.focused .xhr-input-method {
+  display: block;
+}
+
+.xhr-label-url {
   max-width: calc(100% - var(--breakpoint-expression-right-clear-space));
+  color: var(--theme-comment);
   display: inline-block;
   cursor: text;
   flex-grow: 1;
   text-overflow: ellipsis;
-  padding-inline-end: 8px;
+  overflow: hidden;
+  padding: 0px 2px 0px 2px;
+  line-height: 15px;
   font-size: 11px;
 }
 
-.xhr-container .close-btn {
+.xhr-container label {
+  flex-grow: 1;
+  display: flex;
+  padding-inline-end: 36px;
+  align-items: center;
+  overflow-x: hidden;
+}
+
+.xhr-container__close-btn {
   offset-inline-end: 12px;
-  inset-inline-end: 12px;
   offset-inline-start: auto;
-  inset-inline-start: auto;
   position: absolute;
   top: 8px;
 }
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/XHRBreakpoints.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/XHRBreakpoints.js
@@ -30,37 +30,50 @@ type Props = {
   updateXHRBreakpoint: typeof actions.updateXHRBreakpoint
 };
 
 type State = {
   editing: boolean,
   inputValue: string,
   inputMethod: string,
   editIndex: number,
-  focused: boolean
+  focused: boolean,
+  clickedOnFormElement: boolean
 };
 
 // At present, the "Pause on any URL" checkbox creates an xhrBreakpoint
 // of "ANY" with no path, so we can remove that before creating the list
 function getExplicitXHRBreakpoints(xhrBreakpoints) {
   return xhrBreakpoints.filter(bp => bp.path !== "");
 }
 
+const xhrMethods = [
+  "ANY",
+  "GET",
+  "POST",
+  "PUT",
+  "HEAD",
+  "DELETE",
+  "PATCH",
+  "OPTIONS"
+];
+
 class XHRBreakpoints extends Component<Props, State> {
   _input: ?HTMLInputElement;
 
   constructor(props: Props) {
     super(props);
 
     this.state = {
       editing: false,
       inputValue: "",
-      inputMethod: "",
+      inputMethod: "ANY",
       focused: false,
-      editIndex: -1
+      editIndex: -1,
+      clickedOnFormElement: false
     };
   }
 
   componentDidMount() {
     const { showInput } = this.props;
 
     // Ensures that the input is focused when the "+"
     // is clicked while the panel is collapsed
@@ -83,19 +96,31 @@ class XHRBreakpoints extends Component<P
       input.focus();
     }
   }
 
   handleNewSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
     e.preventDefault();
     e.stopPropagation();
 
-    this.props.setXHRBreakpoint(this.state.inputValue, "ANY");
+    const setXHRBreakpoint = function() {
+      this.props.setXHRBreakpoint(
+        this.state.inputValue,
+        this.state.inputMethod
+      );
+      this.hideInput();
+    };
 
-    this.hideInput();
+    // force update inputMethod in state for mochitest purposes
+    // before setting XHR breakpoint
+    this.setState(
+      // $FlowIgnore
+      { inputMethod: e.target.children[1].value },
+      setXHRBreakpoint
+    );
   };
 
   handleExistingSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
     e.preventDefault();
     e.stopPropagation();
 
     const { editIndex, inputValue, inputMethod } = this.state;
     const { xhrBreakpoints } = this.props;
@@ -108,29 +133,66 @@ class XHRBreakpoints extends Component<P
     this.hideInput();
   };
 
   handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
     const target = e.target;
     this.setState({ inputValue: target.value });
   };
 
-  hideInput = () => {
+  handleMethodChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
+    const target = e.target;
     this.setState({
-      focused: false,
-      editing: false,
-      editIndex: -1,
-      inputValue: "",
-      inputMethod: ""
+      focused: true,
+      editing: true,
+      inputMethod: target.value
     });
-    this.props.onXHRAdded();
+  };
+
+  hideInput = () => {
+    if (this.state.clickedOnFormElement) {
+      this.setState({
+        focused: true,
+        clickedOnFormElement: false
+      });
+    } else {
+      this.setState({
+        focused: false,
+        editing: false,
+        editIndex: -1,
+        inputValue: "",
+        inputMethod: "ANY"
+      });
+      this.props.onXHRAdded();
+    }
   };
 
   onFocus = () => {
-    this.setState({ focused: true });
+    this.setState({ focused: true, editing: true });
+  };
+
+  onMouseDown = e => {
+    this.setState({ editing: false, clickedOnFormElement: true });
+  };
+
+  handleTab = e => {
+    if (e.key !== "Tab") {
+      return;
+    }
+
+    if (e.target.nodeName === "INPUT") {
+      this.setState({
+        clickedOnFormElement: true,
+        editing: false
+      });
+    } else if (e.target.nodeName === "SELECT" && !e.shiftKey) {
+      // The user has tabbed off the select and we should
+      // cancel the edit
+      this.hideInput();
+    }
   };
 
   editExpression = index => {
     const { xhrBreakpoints } = this.props;
     const { path, method } = xhrBreakpoints[index];
     this.setState({
       inputValue: path,
       inputMethod: method,
@@ -141,50 +203,53 @@ class XHRBreakpoints extends Component<P
 
   renderXHRInput(onSubmit) {
     const { focused, inputValue } = this.state;
     const placeholder = L10N.getStr("xhrBreakpoints.placeholder");
 
     return (
       <li
         className={classnames("xhr-input-container", { focused })}
-        key="xhr-input"
+        key="xhr-input-container"
       >
         <form className="xhr-input-form" onSubmit={onSubmit}>
           <input
-            className="xhr-input"
+            className="xhr-input-url"
             type="text"
             placeholder={placeholder}
             onChange={this.handleChange}
             onBlur={this.hideInput}
             onFocus={this.onFocus}
             value={inputValue}
+            onKeyDown={this.handleTab}
             ref={c => (this._input = c)}
           />
+          {this.renderMethodSelectElement()}
           <input type="submit" style={{ display: "none" }} />
         </form>
       </li>
     );
   }
+
   handleCheckbox = index => {
     const {
       xhrBreakpoints,
       enableXHRBreakpoint,
       disableXHRBreakpoint
     } = this.props;
     const breakpoint = xhrBreakpoints[index];
     if (breakpoint.disabled) {
       enableXHRBreakpoint(index);
     } else {
       disableXHRBreakpoint(index);
     }
   };
 
   renderBreakpoint = breakpoint => {
-    const { path, text, disabled, method } = breakpoint;
+    const { path, disabled, method } = breakpoint;
     const { editIndex } = this.state;
     const { removeXHRBreakpoint, xhrBreakpoints } = this.props;
 
     // The "pause on any" checkbox
     if (!path) {
       return;
     }
 
@@ -195,33 +260,34 @@ class XHRBreakpoints extends Component<P
 
     if (index === editIndex) {
       return this.renderXHRInput(this.handleExistingSubmit);
     }
 
     return (
       <li
         className="xhr-container"
-        key={path}
+        key={`${path}-${method}`}
         title={path}
         onDoubleClick={(items, options) => this.editExpression(index)}
       >
         <label>
           <input
             type="checkbox"
             className="xhr-checkbox"
             checked={!disabled}
             onChange={() => this.handleCheckbox(index)}
             onClick={ev => ev.stopPropagation()}
           />
-          <div className="xhr-label">{text}</div>
+          <div className="xhr-label-method">{method}</div>
+          <div className="xhr-label-url">{path}</div>
+          <div className="xhr-container__close-btn">
+            <CloseButton handleClick={e => removeXHRBreakpoint(index)} />
+          </div>
         </label>
-        <div className="xhr-container__close-btn">
-          <CloseButton handleClick={e => removeXHRBreakpoint(index)} />
-        </div>
       </li>
     );
   };
 
   renderBreakpoints = () => {
     const { showInput, xhrBreakpoints } = this.props;
     const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints);
 
@@ -249,16 +315,44 @@ class XHRBreakpoints extends Component<P
           label={L10N.getStr("pauseOnAnyXHR")}
           isChecked={shouldPauseOnAny}
           onChange={() => togglePauseOnAny()}
         />
       </div>
     );
   };
 
+  renderMethodOption = method => {
+    return (
+      <option
+        key={method}
+        value={method}
+        // e.stopPropagation() required here since otherwise Firefox triggers 2x
+        // onMouseDown events on <select> upon clicking on an <option>
+        onMouseDown={e => e.stopPropagation()}
+      >
+        {method}
+      </option>
+    );
+  };
+
+  renderMethodSelectElement = () => {
+    return (
+      <select
+        value={this.state.inputMethod}
+        className={"xhr-input-method"}
+        onChange={this.handleMethodChange}
+        onMouseDown={this.onMouseDown}
+        onKeyDown={this.handleTab}
+      >
+        {xhrMethods.map(this.renderMethodOption)}
+      </select>
+    );
+  };
+
   render() {
     return (
       <div>
         {this.renderCheckbox()}
         {this.renderBreakpoints()}
       </div>
     );
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
@@ -0,0 +1,342 @@
+// @flow
+/* 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/>. */
+
+import React from "react";
+import { mount } from "enzyme";
+import XHRBreakpoints from "../XHRBreakpoints";
+
+const xhrMethods = [
+  "ANY",
+  "GET",
+  "POST",
+  "PUT",
+  "HEAD",
+  "DELETE",
+  "PATCH",
+  "OPTIONS"
+];
+
+// default state includes xhrBreakpoints[0] which is the checkbox that
+// enables breaking on any url during an XMLHTTPRequest
+function generateDefaultState(propsOverride) {
+  return {
+    xhrBreakpoints: [
+      {
+        path: "",
+        method: "ANY",
+        disabled: false,
+        loading: false,
+        text: 'URL contains ""'
+      }
+    ],
+    ...propsOverride
+  };
+}
+
+function renderXHRBreakpointsComponent(propsOverride) {
+  const props = generateDefaultState(propsOverride);
+  const xhrBreakpointsComponent = mount(
+    // $FlowIgnore
+    <XHRBreakpoints.WrappedComponent {...props} />
+  );
+  return xhrBreakpointsComponent;
+}
+
+describe("XHR Breakpoints", function() {
+  it("should render with 0 expressions passed from props", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+    expect(xhrBreakpointsComponent).toMatchSnapshot();
+  });
+
+  it("should render with 8 expressions passed from props", function() {
+    const allXHRBreakpointMethods = {
+      xhrBreakpoints: [
+        {
+          path: "",
+          method: "ANY",
+          disabled: false,
+          loading: false,
+          text: 'URL contains ""'
+        },
+        {
+          path: "this is any",
+          method: "ANY",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is any"'
+        },
+        {
+          path: "this is get",
+          method: "GET",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is get"'
+        },
+        {
+          path: "this is post",
+          method: "POST",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is post"'
+        },
+        {
+          path: "this is put",
+          method: "PUT",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is put"'
+        },
+        {
+          path: "this is head",
+          method: "HEAD",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is head"'
+        },
+        {
+          path: "this is delete",
+          method: "DELETE",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is delete"'
+        },
+        {
+          path: "this is patch",
+          method: "PATCH",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is patch"'
+        },
+        {
+          path: "this is options",
+          method: "OPTIONS",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is options"'
+        }
+      ]
+    };
+
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent(
+      allXHRBreakpointMethods
+    );
+    expect(xhrBreakpointsComponent).toMatchSnapshot();
+  });
+
+  it("should display xhr-input-method on click", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+    var xhrInputContainer = xhrBreakpointsComponent.find(
+      ".xhr-input-container"
+    );
+    expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+  });
+
+  it("should have focused and editing default to false", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+    expect(xhrBreakpointsComponent.state("focused")).toBe(false);
+    expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+  });
+
+  it("should have state {..focused: true, editing: true} on focus", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+    expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+    expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+  });
+
+  // shifting focus from .xhr-input to any other element apart from
+  // .xhr-input-method should unrender .xhr-input-method
+  it("shifting focus should unrender XHR methods", function() {
+    const propsOverride = {
+      onXHRAdded: jest.fn,
+      togglePauseOnAny: jest.fn
+    };
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent(
+      propsOverride
+    );
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+    var xhrInputContainer = xhrBreakpointsComponent.find(
+      ".xhr-input-container"
+    );
+    expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+
+    xhrBreakpointsComponent
+      .find(".breakpoints-exceptions-options")
+      .simulate("mousedown");
+    expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+    expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+    expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("blur");
+    expect(xhrBreakpointsComponent.state("focused")).toBe(false);
+    expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+    expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+    xhrBreakpointsComponent
+      .find(".breakpoints-exceptions-options")
+      .simulate("click");
+
+    xhrInputContainer = xhrBreakpointsComponent.find(".xhr-input-container");
+    expect(xhrInputContainer.hasClass("focused")).not.toBeTruthy();
+  });
+
+  // shifting focus from .xhr-input to .xhr-input-method
+  // should not unrender .xhr-input-method
+  it("shifting focus to XHR methods should not unrender", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+    xhrBreakpointsComponent.find(".xhr-input-method").simulate("mousedown");
+    expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+    expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+    expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(true);
+
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("blur");
+    expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+    expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+    expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+    xhrBreakpointsComponent.find(".xhr-input-method").simulate("click");
+    var xhrInputContainer = xhrBreakpointsComponent.find(
+      ".xhr-input-container"
+    );
+    expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+  });
+
+  it("should have all 8 methods available as options", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+    const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+    expect(xhrInputMethod.children()).toHaveLength(8);
+
+    const actualXHRMethods = [];
+    const expectedXHRMethods = xhrMethods;
+
+    // fill the actualXHRMethods array with actual methods displayed in DOM
+    for (let i = 0; i < xhrInputMethod.children().length; i++) {
+      actualXHRMethods.push(xhrInputMethod.childAt(i).key());
+    }
+
+    // check each expected XHR Method to see if they match the actual methods
+    expectedXHRMethods.forEach((expectedMethod, i) => {
+      function compareMethods(actualMethod) {
+        return expectedMethod === actualMethod;
+      }
+      expect(actualXHRMethods.find(compareMethods)).toBeTruthy();
+    });
+  });
+
+  it("should return focus to input box after selecting a method", function() {
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+
+    // focus starts off at .xhr-input
+    xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+    // click on method options and select GET
+    const methodEvent = { target: { value: "GET" } };
+    xhrBreakpointsComponent.find(".xhr-input-method").simulate("mousedown");
+    expect(xhrBreakpointsComponent.state("inputMethod")).toBe("ANY");
+    expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+    xhrBreakpointsComponent
+      .find(".xhr-input-method")
+      .simulate("change", methodEvent);
+
+    // if state.editing changes from false to true, infer that
+    // this._input.focus() is called, which shifts focus back to input box
+    expect(xhrBreakpointsComponent.state("inputMethod")).toBe("GET");
+    expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+  });
+
+  it("should submit the URL and method when adding a breakpoint", function() {
+    const setXHRBreakpointCallback = jest.fn();
+    const propsOverride = {
+      setXHRBreakpoint: setXHRBreakpointCallback,
+      onXHRAdded: jest.fn()
+    };
+    const mockEvent = {
+      preventDefault: jest.fn(),
+      stopPropagation: jest.fn()
+    };
+    const availableXHRMethods = xhrMethods;
+    expect(availableXHRMethods.length > 0).toBeTruthy();
+
+    // check each of the available methods to see whether
+    // adding them as a method to a new breakpoint works as expected
+    availableXHRMethods.forEach(function(method) {
+      const xhrBreakpointsComponent = renderXHRBreakpointsComponent(
+        propsOverride
+      );
+      xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+      const urlValue = `${method.toLowerCase()}URLValue`;
+
+      // simulate DOM event adding urlValue to .xhr-input
+      const xhrInput = xhrBreakpointsComponent.find(".xhr-input-url");
+      xhrInput.simulate("change", { target: { value: urlValue } });
+
+      // simulate DOM event adding the input method to .xhr-input-method
+      const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+      xhrInputMethod.simulate("change", { target: { value: method } });
+
+      xhrBreakpointsComponent.find("form").simulate("submit", mockEvent);
+      expect(setXHRBreakpointCallback).toHaveBeenCalledWith(urlValue, method);
+    });
+  });
+
+  it("should submit the URL and method when editing a breakpoint", function() {
+    const setXHRBreakpointCallback = jest.fn();
+    const mockEvent = {
+      preventDefault: jest.fn(),
+      stopPropagation: jest.fn()
+    };
+    const propsOverride = {
+      updateXHRBreakpoint: setXHRBreakpointCallback,
+      onXHRAdded: jest.fn(),
+      xhrBreakpoints: [
+        {
+          path: "",
+          method: "ANY",
+          disabled: false,
+          loading: false,
+          text: 'URL contains ""'
+        },
+        {
+          path: "this is GET",
+          method: "GET",
+          disabled: false,
+          loading: false,
+          text: 'URL contains "this is get"'
+        }
+      ]
+    };
+    const xhrBreakpointsComponent = renderXHRBreakpointsComponent(
+      propsOverride
+    );
+
+    // load xhrBreakpoints pane with one existing xhrBreakpoint
+    const existingXHRbreakpoint = xhrBreakpointsComponent.find(
+      ".xhr-container"
+    );
+    expect(existingXHRbreakpoint).toHaveLength(1);
+
+    // double click on existing breakpoint
+    existingXHRbreakpoint.simulate("doubleclick");
+    const xhrInput = xhrBreakpointsComponent.find(".xhr-input-url");
+    xhrInput.simulate("focus");
+
+    // change inputs and submit form
+    const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+    xhrInput.simulate("change", { target: { value: "POSTURLValue" } });
+    xhrInputMethod.simulate("change", { target: { value: "POST" } });
+    xhrBreakpointsComponent.find("form").simulate("submit", mockEvent);
+    expect(setXHRBreakpointCallback).toHaveBeenCalledWith(
+      1,
+      "POSTURLValue",
+      "POST"
+    );
+  });
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
@@ -0,0 +1,613 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`XHR Breakpoints should render with 0 expressions passed from props 1`] = `
+<XHRBreakpoints
+  xhrBreakpoints={
+    Array [
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "ANY",
+        "path": "",
+        "text": "URL contains \\"\\"",
+      },
+    ]
+  }
+>
+  <div>
+    <div
+      className="breakpoints-exceptions-options empty"
+    >
+      <ExceptionOption
+        className="breakpoints-exceptions"
+        label="Pause on any URL"
+        onChange={[Function]}
+      >
+        <div
+          className="breakpoints-exceptions"
+          onClick={[Function]}
+        >
+          <input
+            checked=""
+            onChange={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="breakpoint-exceptions-label"
+          >
+            Pause on any URL
+          </div>
+        </div>
+      </ExceptionOption>
+    </div>
+    <ul
+      className="pane expressions-list"
+    >
+      <li
+        className="xhr-input-container"
+        key="xhr-input-container"
+      >
+        <form
+          className="xhr-input-form"
+          onSubmit={[Function]}
+        >
+          <input
+            className="xhr-input-url"
+            onBlur={[Function]}
+            onChange={[Function]}
+            onFocus={[Function]}
+            onKeyDown={[Function]}
+            placeholder="Break when URL contains"
+            type="text"
+            value=""
+          />
+          <select
+            className="xhr-input-method"
+            onChange={[Function]}
+            onKeyDown={[Function]}
+            onMouseDown={[Function]}
+            value="ANY"
+          >
+            <option
+              key="ANY"
+              onMouseDown={[Function]}
+              value="ANY"
+            >
+              ANY
+            </option>
+            <option
+              key="GET"
+              onMouseDown={[Function]}
+              value="GET"
+            >
+              GET
+            </option>
+            <option
+              key="POST"
+              onMouseDown={[Function]}
+              value="POST"
+            >
+              POST
+            </option>
+            <option
+              key="PUT"
+              onMouseDown={[Function]}
+              value="PUT"
+            >
+              PUT
+            </option>
+            <option
+              key="HEAD"
+              onMouseDown={[Function]}
+              value="HEAD"
+            >
+              HEAD
+            </option>
+            <option
+              key="DELETE"
+              onMouseDown={[Function]}
+              value="DELETE"
+            >
+              DELETE
+            </option>
+            <option
+              key="PATCH"
+              onMouseDown={[Function]}
+              value="PATCH"
+            >
+              PATCH
+            </option>
+            <option
+              key="OPTIONS"
+              onMouseDown={[Function]}
+              value="OPTIONS"
+            >
+              OPTIONS
+            </option>
+          </select>
+          <input
+            style={
+              Object {
+                "display": "none",
+              }
+            }
+            type="submit"
+          />
+        </form>
+      </li>
+    </ul>
+  </div>
+</XHRBreakpoints>
+`;
+
+exports[`XHR Breakpoints should render with 8 expressions passed from props 1`] = `
+<XHRBreakpoints
+  xhrBreakpoints={
+    Array [
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "ANY",
+        "path": "",
+        "text": "URL contains \\"\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "ANY",
+        "path": "this is any",
+        "text": "URL contains \\"this is any\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "GET",
+        "path": "this is get",
+        "text": "URL contains \\"this is get\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "POST",
+        "path": "this is post",
+        "text": "URL contains \\"this is post\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "PUT",
+        "path": "this is put",
+        "text": "URL contains \\"this is put\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "HEAD",
+        "path": "this is head",
+        "text": "URL contains \\"this is head\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "DELETE",
+        "path": "this is delete",
+        "text": "URL contains \\"this is delete\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "PATCH",
+        "path": "this is patch",
+        "text": "URL contains \\"this is patch\\"",
+      },
+      Object {
+        "disabled": false,
+        "loading": false,
+        "method": "OPTIONS",
+        "path": "this is options",
+        "text": "URL contains \\"this is options\\"",
+      },
+    ]
+  }
+>
+  <div>
+    <div
+      className="breakpoints-exceptions-options"
+    >
+      <ExceptionOption
+        className="breakpoints-exceptions"
+        label="Pause on any URL"
+        onChange={[Function]}
+      >
+        <div
+          className="breakpoints-exceptions"
+          onClick={[Function]}
+        >
+          <input
+            checked=""
+            onChange={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="breakpoint-exceptions-label"
+          >
+            Pause on any URL
+          </div>
+        </div>
+      </ExceptionOption>
+    </div>
+    <ul
+      className="pane expressions-list"
+    >
+      <li
+        className="xhr-container"
+        key="this is any-ANY"
+        onDoubleClick={[Function]}
+        title="this is any"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            ANY
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is any
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is get-GET"
+        onDoubleClick={[Function]}
+        title="this is get"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            GET
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is get
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is post-POST"
+        onDoubleClick={[Function]}
+        title="this is post"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            POST
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is post
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is put-PUT"
+        onDoubleClick={[Function]}
+        title="this is put"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            PUT
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is put
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is head-HEAD"
+        onDoubleClick={[Function]}
+        title="this is head"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            HEAD
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is head
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is delete-DELETE"
+        onDoubleClick={[Function]}
+        title="this is delete"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            DELETE
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is delete
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is patch-PATCH"
+        onDoubleClick={[Function]}
+        title="this is patch"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            PATCH
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is patch
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+      <li
+        className="xhr-container"
+        key="this is options-OPTIONS"
+        onDoubleClick={[Function]}
+        title="this is options"
+      >
+        <label>
+          <input
+            checked={true}
+            className="xhr-checkbox"
+            onChange={[Function]}
+            onClick={[Function]}
+            type="checkbox"
+          />
+          <div
+            className="xhr-label-method"
+          >
+            OPTIONS
+          </div>
+          <div
+            className="xhr-label-url"
+          >
+            this is options
+          </div>
+          <div
+            className="xhr-container__close-btn"
+          >
+            <CloseButton
+              handleClick={[Function]}
+            >
+              <button
+                className="close-btn"
+                onClick={[Function]}
+              >
+                <AccessibleImage
+                  className="close"
+                >
+                  <span
+                    className="img close"
+                  />
+                </AccessibleImage>
+              </button>
+            </CloseButton>
+          </div>
+        </label>
+      </li>
+    </ul>
+  </div>
+</XHRBreakpoints>
+`;
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-breakpoints.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-breakpoints.js
@@ -1,69 +1,82 @@
 /* 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/>. */
 
- async function addXHRBreakpoint(dbg, text) {
-  info("Adding a XHR breakpoint");
+async function addXHRBreakpoint(dbg, text, method) {
+  info(`Adding a XHR breakpoint for pattern ${text} and method ${method}`);
 
   const plusIcon = findElementWithSelector(dbg, ".xhr-breakpoints-pane .plus");
   if (plusIcon) {
     plusIcon.click();
   }
-  findElementWithSelector(dbg, ".xhr-input").focus();
+  findElementWithSelector(dbg, ".xhr-input-url").focus();
   type(dbg, text);
+
+  if (method) {
+    findElementWithSelector(dbg, ".xhr-input-method").value = method;
+  }
+
   pressKey(dbg, "Enter");
 
   await waitForDispatch(dbg, "SET_XHR_BREAKPOINT");
 }
 
 async function removeXHRBreakpoint(dbg, index) {
   info("Removing a XHR breakpoint");
 
-  const closeButtons = dbg.win.document.querySelectorAll(".xhr-breakpoints-pane .close-btn");
+  const closeButtons = dbg.win.document.querySelectorAll(
+    ".xhr-breakpoints-pane .close-btn"
+  );
   if (closeButtons[index]) {
     closeButtons[index].click();
   }
 
   await waitForDispatch(dbg, "REMOVE_XHR_BREAKPOINT");
 }
 
 function getXHRBreakpointsElements(dbg) {
-  return [...dbg.win.document.querySelectorAll(".xhr-breakpoints-pane .xhr-container")];
+  return [
+    ...dbg.win.document.querySelectorAll(".xhr-breakpoints-pane .xhr-container")
+  ];
 }
 
 function getXHRBreakpointLabels(elements) {
   return elements.map(element => element.title);
 }
 
 function getXHRBreakpointCheckbox(dbg) {
-  return findElementWithSelector(dbg, ".xhr-breakpoints-pane .breakpoints-exceptions input");
+  return findElementWithSelector(
+    dbg,
+    ".xhr-breakpoints-pane .breakpoints-exceptions input"
+  );
 }
 
 async function clickPauseOnAny(dbg, expectedEvent) {
   getXHRBreakpointCheckbox(dbg).click();
   await waitForDispatch(dbg, expectedEvent);
 }
 
-// Tests that a basic XHR breakpoint works for get and POST is ignored
 add_task(async function() {
   const dbg = await initDebugger("doc-xhr.html", "fetch.js");
-  await dbg.actions.setXHRBreakpoint("doc", "GET");
+
+  await addXHRBreakpoint(dbg, "doc", "GET");
+
   invokeInTab("main", "doc-xhr.html");
   await waitForPaused(dbg);
   assertPausedLocation(dbg);
-  resume(dbg);
+  await resume(dbg);
 
   await dbg.actions.removeXHRBreakpoint(0);
-  invokeInTab("main", "doc-xhr.html");
+  await invokeInTab("main", "doc-xhr.html");
   assertNotPaused(dbg);
 
-  await dbg.actions.setXHRBreakpoint("doc-xhr.html", "POST");
-  invokeInTab("main", "doc");
+  await addXHRBreakpoint(dbg, "doc", "POST");
+  await invokeInTab("main", "doc-xhr.html");
   assertNotPaused(dbg);
 });
 
 // Tests the "pause on any URL" checkbox works properly
 add_task(async function() {
   const dbg = await initDebugger("doc-xhr.html", "fetch.js");
 
   // Enable pause on any URL
@@ -102,19 +115,15 @@ add_task(async function() {
   await addXHRBreakpoint(dbg, "3");
   await addXHRBreakpoint(dbg, "4");
 
   // Remove "2"
   await removeXHRBreakpoint(dbg, 1);
 
   const listItems = getXHRBreakpointsElements(dbg);
   is(listItems.length, 3, "3 XHR breakpoints display in list");
-  is(
-    pauseOnAnyCheckbox.checked, true,
-    "The pause on any is still checked"
-  );
+  is(pauseOnAnyCheckbox.checked, true, "The pause on any is still checked");
   is(
     getXHRBreakpointLabels(listItems).join(""),
     "134",
     "Only the desired breakpoint was removed"
   );
-
 });
--- a/devtools/client/debugger/new/test/mochitest/examples/fetch.js
+++ b/devtools/client/debugger/new/test/mochitest/examples/fetch.js
@@ -1,6 +1,5 @@
 const doc = "doc-xhr.html";
 
-
 function main(url) {
-    fetch(url).then(console.log);
+  fetch(url).then(console.log);
 }