Bug 1486870 - [release] Make the Frames component copy-friendly (#7723); r=jlast.
authorNicolas Chevobbe <nchevobbe@users.noreply.github.com>
Fri, 25 Jan 2019 17:14:42 +0000
changeset 515477 c688c38605f9f12542afae478836c741d827fd03
parent 515476 d32a5f9aaec5103290f63abc66bb727de9edf380
child 515478 2fbbe85bb9923a7eb11ed36dd07343689dc8d071
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)
reviewersjlast
bugs1486870
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 1486870 - [release] Make the Frames component copy-friendly (#7723); r=jlast. Differential Revision: https://phabricator.services.mozilla.com/D17375
devtools/client/debugger/new/dist/debugger.css
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Frame.js
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/FrameIndent.js
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Frames.css
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/index.js
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/moz.build
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
devtools/client/debugger/new/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
devtools/client/debugger/new/test/mochitest/helpers.js
devtools/client/shared/components/reps/reps.js
--- a/devtools/client/debugger/new/dist/debugger.css
+++ b/devtools/client/debugger/new/dist/debugger.css
@@ -3744,47 +3744,52 @@ html[dir="rtl"] .breakpoints-list .break
 
 .expression-input {
   max-width: 50%;
 }
 /* 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/>. */
 
-.frames ul .frames-group .group,
-.frames ul .frames-group .group .location {
+.frames [role="list"] .frames-group .group,
+.frames [role="list"] .frames-group .group .location {
   font-weight: 500;
   cursor: default;
   /*
    * direction:rtl is set in Frames.css to overflow the location text from the
    * start. Here we need to reset it in order to display the framework icon
    * after the framework name.
    */
   direction: ltr;
 }
 
-.frames ul .frames-group.expanded .group,
-.frames ul .frames-group.expanded .group .location {
+.frames [role="list"] .frames-group.expanded .group,
+.frames [role="list"] .frames-group.expanded .group .location {
   color: var(--theme-highlight-blue);
 }
 
-.frames ul .frames-group.expanded .react path {
+.frames [role="list"] .frames-group.expanded .react path {
   fill: var(--theme-highlight-blue);
 }
 
-.frames ul .frames-group .frames-list li {
+.frames [role="list"] .frames-group .frames-list [role="listitem"] {
   padding-left: 30px;
 }
 
-.frames ul .frames-group .frames-list {
+.frames [role="list"] .frames-group .frames-list {
   border-top: 1px solid var(--theme-splitter-color);
   border-bottom: 1px solid var(--theme-splitter-color);
 }
 
-.frames ul .frames-group.expanded .badge {
+/* We don't want to display those as flex since only the name is displayed */
+.frames [role="list"] .frames-group .frames-list [role="listitem"] {
+  display: block;
+}
+
+.frames [role="list"] .frames-group.expanded .badge {
   color: var(--theme-highlight-blue);
 }
 
 .group-description-name {
   padding-left: 5px;
 }
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
@@ -3839,36 +3844,36 @@ html[dir="rtl"] .breakpoints-list .break
   color: var(--theme-graphs-red);
   font-weight: bold;
   font-style: normal;
 }
 /* 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/>. */
 
-.frames ul {
+.frames [role="list"] {
   list-style: none;
   margin-top: 4px;
   padding: 0;
 }
 
-.frames ul li {
+.frames [role="list"] [role="listitem"] {
   padding: 2px 10px 2px 20px;
   overflow: hidden;
   display: flex;
   justify-content: space-between;
   column-gap: 0.5em;
   flex-direction: row;
   align-items: center;
   margin: 0;
   max-width: 100%;
   flex-wrap: wrap;
 }
 
-.frames ul li * {
+.frames [role="list"] [role="listitem"] * {
   -moz-user-select: none;
   user-select: none;
 }
 
 .frames .badge {
   flex-shrink: 0;
   margin-right: 4px;
 }
@@ -3895,36 +3900,36 @@ html[dir="rtl"] .breakpoints-list .break
   opacity: 0.6;
 }
 
 .frames .title {
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
-.frames ul li:hover,
-.frames ul li:focus {
+.frames [role="list"] [role="listitem"]hover,
+.frames [role="list"] [role="listitem"]focus {
   background-color: var(--theme-toolbar-background-alt);
 }
 
-.theme-dark .frames ul li:focus {
+.theme-dark .frames [role="list"] [role="listitem"]focus {
   background-color: var(--theme-tab-toolbar-background);
 }
 
-.frames ul li.selected {
+.frames [role="list"] [role="listitem"].selected {
   background-color: var(--theme-selection-background);
   color: white;
 }
 
-.frames ul li.selected i.annotation-logo svg path {
+.frames [role="list"] [role="listitem"].selected i.annotation-logo svg path {
   fill: white;
 }
 
-:root.theme-light .frames ul li.selected .location,
-:root.theme-dark .frames ul li.selected .location {
+:root.theme-light .frames [role="list"] [role="listitem"].selected .location,
+:root.theme-dark .frames [role="list"] [role="listitem"].selected .location {
   color: white;
 }
 
 .show-more-container {
   display: flex;
   min-height: 24px;
   padding: 4px 0;
 }
@@ -3948,16 +3953,25 @@ html[dir="rtl"] .breakpoints-list .break
   mask-size: 100%;
   display: inline-block;
   width: 12px;
 }
 
 :root.theme-dark .annotation-logo:not(.angular) svg path {
   fill: var(--theme-highlight-blue);
 }
+
+/* Some elements are added to the DOM only to be printed into the clipboard
+   when the user copy some elements. We don't want those elements to mess with
+   the layout so we put them outside of the screen
+*/
+.clipboard-only {
+  position: absolute;
+  left: -9999px;
+}
 /* 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/>. */
 
 .workers-list * {
   user-select: none;
 }
 
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Frame.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Frame.js
@@ -7,16 +7,17 @@ import React, { Component } from "react"
 import PropTypes from "prop-types";
 
 import classNames from "classnames";
 
 import AccessibleImage from "../../shared/AccessibleImage";
 import { formatDisplayName } from "../../../utils/pause/frames";
 import { getFilename, getFileURL } from "../../../utils/source";
 import FrameMenu from "./FrameMenu";
+import FrameIndent from "./FrameIndent";
 
 import type { Frame } from "../../../types";
 import type { LocalFrame } from "./types";
 
 type FrameTitleProps = {
   frame: Frame,
   options: Object,
   l10n: Object
@@ -67,17 +68,18 @@ type FrameComponentProps = {
   toggleFrameworkGrouping: Function,
   selectFrame: Function,
   frameworkGroupingOn: boolean,
   hideLocation: boolean,
   shouldMapDisplayName: boolean,
   toggleBlackBox: Function,
   displayFullUrl: boolean,
   getFrameTitle?: string => string,
-  disableContextMenu: boolean
+  disableContextMenu: boolean,
+  selectable: boolean
 };
 
 export default class FrameComponent extends Component<FrameComponentProps> {
   static defaultProps = {
     hideLocation: false,
     shouldMapDisplayName: true,
     disableContextMenu: false
   };
@@ -123,53 +125,52 @@ export default class FrameComponent exte
   render() {
     const {
       frame,
       selectedFrame,
       hideLocation,
       shouldMapDisplayName,
       displayFullUrl,
       getFrameTitle,
-      disableContextMenu
+      disableContextMenu,
+      selectable
     } = this.props;
     const { l10n } = this.context;
 
     const className = classNames("frame", {
       selected: selectedFrame && selectedFrame.id === frame.id
     });
 
     const title = getFrameTitle
       ? getFrameTitle(
           `${getFileURL(frame.source, false)}:${frame.location.line}`
         )
       : undefined;
 
-    const tabChar = "\t";
-    const newLineChar = "\n";
-
     return (
-      <li
+      <div
+        role="listitem"
         key={frame.id}
         className={className}
         onMouseDown={e => this.onMouseDown(e, frame, selectedFrame)}
         onKeyUp={e => this.onKeyUp(e, frame, selectedFrame)}
         onContextMenu={disableContextMenu ? null : e => this.onContextMenu(e)}
         tabIndex={0}
         title={title}
       >
-        {tabChar}
+        {selectable && <FrameIndent />}
         <FrameTitle
           frame={frame}
           options={{ shouldMapDisplayName }}
           l10n={l10n}
         />
-        {!hideLocation && " "}
+        {!hideLocation && <span className="clipboard-only"> </span>}
         {!hideLocation && (
           <FrameLocation frame={frame} displayFullUrl={displayFullUrl} />
         )}
-        {newLineChar}
-      </li>
+        {selectable && <br className="clipboard-only" />}
+      </div>
     );
   }
 }
 
 FrameComponent.displayName = "Frame";
 FrameComponent.contextTypes = { l10n: PropTypes.object };
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/FrameIndent.js
@@ -0,0 +1,13 @@
+/* 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";
+
+export default function FrameIndent() {
+  return (
+    <span className="frame-indent clipboard-only">
+      &nbsp;&nbsp;&nbsp;&nbsp;
+    </span>
+  );
+}
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Frames.css
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Frames.css
@@ -1,32 +1,32 @@
 /* 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/>. */
 
-.frames ul {
+.frames [role="list"]l {
   list-style: none;
   margin-top: 4px;
   padding: 0;
 }
 
-.frames ul li {
+.frames [role="list"] [role="listitem"] {
   padding: 2px 10px 2px 20px;
   overflow: hidden;
   display: flex;
   justify-content: space-between;
   column-gap: 0.5em;
   flex-direction: row;
   align-items: center;
   margin: 0;
   max-width: 100%;
   flex-wrap: wrap;
 }
 
-.frames ul li * {
+.frames [role="list"] [role="listitem"] * {
   -moz-user-select: none;
   user-select: none;
 }
 
 .frames .badge {
   flex-shrink: 0;
   margin-right: 4px;
 }
@@ -53,36 +53,36 @@
   opacity: 0.6;
 }
 
 .frames .title {
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
-.frames ul li:hover,
-.frames ul li:focus {
+[role="list"] [role="listitem"]hover,
+[role="list"] [role="listitem"]focus {
   background-color: var(--theme-toolbar-background-alt);
 }
 
-.theme-dark .frames ul li:focus {
+.theme-dark [role="list"] [role="listitem"]focus {
   background-color: var(--theme-tab-toolbar-background);
 }
 
-.frames ul li.selected {
+.frames [role="list"] [role="listitem"].selected {
   background-color: var(--theme-selection-background);
   color: white;
 }
 
-.frames ul li.selected i.annotation-logo svg path {
+.frames [role="list"] [role="listitem"].selected i.annotation-logo svg path {
   fill: white;
 }
 
-:root.theme-light .frames ul li.selected .location,
-:root.theme-dark .frames ul li.selected .location {
+:root.theme-light .frames [role="list"] [role="listitem"].selected .location,
+:root.theme-dark .frames [role="list"] [role="listitem"].selected .location {
   color: white;
 }
 
 .show-more-container {
   display: flex;
   min-height: 24px;
   padding: 4px 0;
 }
@@ -106,8 +106,17 @@
   mask-size: 100%;
   display: inline-block;
   width: 12px;
 }
 
 :root.theme-dark .annotation-logo:not(.angular) svg path {
   fill: var(--theme-highlight-blue);
 }
+
+/* Some elements are added to the DOM only to be printed into the clipboard
+   when the user copy some elements. We don't want those elements to mess with
+   the layout so we put them outside of the screen
+*/
+.clipboard-only {
+  position: absolute;
+  left: -9999px;
+}
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
@@ -6,23 +6,23 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import classNames from "classnames";
 
 import { getLibraryFromUrl } from "../../../utils/pause/frames";
 
 import FrameMenu from "./FrameMenu";
 import AccessibleImage from "../../shared/AccessibleImage";
+import FrameComponent from "./Frame";
 
 import "./Group.css";
 
-import FrameComponent from "./Frame";
-
 import type { LocalFrame } from "./types";
 import Badge from "../../shared/Badge";
+import FrameIndent from "./FrameIndent";
 
 type FrameLocationProps = { frame: LocalFrame, expanded: boolean };
 function FrameLocation({ frame, expanded }: FrameLocationProps) {
   const library = frame.library || getLibraryFromUrl(frame);
   if (!library) {
     return null;
   }
 
@@ -43,17 +43,18 @@ type Props = {
   selectedFrame: LocalFrame,
   selectFrame: Function,
   toggleFrameworkGrouping: Function,
   copyStackTrace: Function,
   toggleBlackBox: Function,
   frameworkGroupingOn: boolean,
   displayFullUrl: boolean,
   getFrameTitle?: string => string,
-  disableContextMenu: boolean
+  disableContextMenu: boolean,
+  selectable: boolean
 };
 
 type State = {
   expanded: boolean
 };
 
 export default class Group extends Component<Props, State> {
   toggleFrames: Function;
@@ -91,68 +92,79 @@ export default class Group extends Compo
       selectFrame,
       selectedFrame,
       toggleFrameworkGrouping,
       frameworkGroupingOn,
       toggleBlackBox,
       copyStackTrace,
       displayFullUrl,
       getFrameTitle,
-      disableContextMenu
+      disableContextMenu,
+      selectable
     } = this.props;
 
     const { expanded } = this.state;
     if (!expanded) {
       return null;
     }
 
     return (
       <div className="frames-list">
-        {group.map(frame => (
-          <FrameComponent
-            copyStackTrace={copyStackTrace}
-            frame={frame}
-            frameworkGroupingOn={frameworkGroupingOn}
-            hideLocation={true}
-            key={frame.id}
-            selectedFrame={selectedFrame}
-            selectFrame={selectFrame}
-            shouldMapDisplayName={false}
-            toggleBlackBox={toggleBlackBox}
-            toggleFrameworkGrouping={toggleFrameworkGrouping}
-            displayFullUrl={displayFullUrl}
-            getFrameTitle={getFrameTitle}
-            disableContextMenu={disableContextMenu}
-          />
-        ))}
+        {group.reduce((acc, frame) => {
+          if (selectable) {
+            acc.push(<FrameIndent />);
+          }
+          return acc.concat(
+            <FrameComponent
+              copyStackTrace={copyStackTrace}
+              frame={frame}
+              frameworkGroupingOn={frameworkGroupingOn}
+              hideLocation={true}
+              key={frame.id}
+              selectedFrame={selectedFrame}
+              selectFrame={selectFrame}
+              shouldMapDisplayName={false}
+              toggleBlackBox={toggleBlackBox}
+              toggleFrameworkGrouping={toggleFrameworkGrouping}
+              displayFullUrl={displayFullUrl}
+              getFrameTitle={getFrameTitle}
+              disableContextMenu={disableContextMenu}
+              selectable={selectable}
+            />
+          );
+        }, [])}
       </div>
     );
   }
 
   renderDescription() {
     const { l10n } = this.context;
+    const { selectable, group } = this.props;
 
-    const frame = this.props.group[0];
+    const frame = group[0];
     const expanded = this.state.expanded;
     const l10NEntry = this.state.expanded
       ? "callStack.group.collapseTooltip"
       : "callStack.group.expandTooltip";
     const title = l10n.getFormatStr(l10NEntry, frame.library);
 
     return (
-      <li
+      <div
+        role="listitem"
         key={frame.id}
         className={classNames("group")}
         onClick={this.toggleFrames}
         tabIndex={0}
         title={title}
       >
+        {selectable && <FrameIndent />}
         <FrameLocation frame={frame} expanded={expanded} />
+        {selectable && <span className="clipboard-only"> </span>}
         <Badge>{this.props.group.length}</Badge>
-      </li>
+      </div>
     );
   }
 
   render() {
     const { expanded } = this.state;
     const { disableContextMenu } = this.props;
     return (
       <div
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/index.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/index.js
@@ -38,17 +38,18 @@ type Props = {
   selectedFrame: Object,
   why: Why,
   selectFrame: Function,
   toggleBlackBox: Function,
   toggleFrameworkGrouping: Function,
   disableFrameTruncate: boolean,
   disableContextMenu: boolean,
   displayFullUrl: boolean,
-  getFrameTitle?: string => string
+  getFrameTitle?: string => string,
+  selectable?: boolean
 };
 
 type State = {
   showAllFrames: boolean
 };
 
 class Frames extends Component<Props, State> {
   renderFrames: Function;
@@ -115,57 +116,63 @@ class Frames extends Component<Props, St
   renderFrames(frames: LocalFrame[]) {
     const {
       selectFrame,
       selectedFrame,
       toggleBlackBox,
       frameworkGroupingOn,
       displayFullUrl,
       getFrameTitle,
-      disableContextMenu
+      disableContextMenu,
+      selectable = false
     } = this.props;
 
     const framesOrGroups = this.truncateFrames(this.collapseFrames(frames));
     type FrameOrGroup = LocalFrame | LocalFrame[];
 
+    // We're not using a <ul> because it adds new lines before and after when
+    // the user copies the trace. Needed for the console which has several
+    // places where we don't want to have those new lines.
     return (
-      <ul>
+      <div role="list">
         {framesOrGroups.map(
           (frameOrGroup: FrameOrGroup) =>
             frameOrGroup.id ? (
               <FrameComponent
                 frame={frameOrGroup}
                 toggleFrameworkGrouping={this.toggleFrameworkGrouping}
                 copyStackTrace={this.copyStackTrace}
                 frameworkGroupingOn={frameworkGroupingOn}
                 selectFrame={selectFrame}
                 selectedFrame={selectedFrame}
                 toggleBlackBox={toggleBlackBox}
                 key={String(frameOrGroup.id)}
                 displayFullUrl={displayFullUrl}
                 getFrameTitle={getFrameTitle}
                 disableContextMenu={disableContextMenu}
+                selectable={selectable}
               />
             ) : (
               <Group
                 group={frameOrGroup}
                 toggleFrameworkGrouping={this.toggleFrameworkGrouping}
                 copyStackTrace={this.copyStackTrace}
                 frameworkGroupingOn={frameworkGroupingOn}
                 selectFrame={selectFrame}
                 selectedFrame={selectedFrame}
                 toggleBlackBox={toggleBlackBox}
                 key={frameOrGroup[0].id}
                 displayFullUrl={displayFullUrl}
                 getFrameTitle={getFrameTitle}
                 disableContextMenu={disableContextMenu}
+                selectable={selectable}
               />
             )
         )}
-      </ul>
+      </div>
     );
   }
 
   renderToggleButton(frames: LocalFrame[]) {
     const { l10n } = this.context;
     const buttonMessage = this.state.showAllFrames
       ? l10n.getStr("callStack.collapse")
       : l10n.getStr("callStack.expand");
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/moz.build
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/moz.build
@@ -4,13 +4,14 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
 
 ]
 
 DebuggerModules(
     'Frame.js',
+    'FrameIndent.js',
     'FrameMenu.js',
     'Group.js',
     'index.js',
     'WhyPaused.js',
 )
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
@@ -76,34 +76,34 @@ describe("Frame", () => {
       },
       displayName: "renderFoo",
       location: {
         line: 10
       }
     };
 
     const component = mount(<Frame frame={frame} />);
-    expect(component.text()).toBe("\trenderFoo foo-view.js:10\n");
+    expect(component.text()).toBe("renderFoo foo-view.js:10");
   });
 
   it("full URL", () => {
     const url = `https://${"a".repeat(100)}.com/assets/src/js/foo-view.js`;
     const frame = {
       id: 1,
       source: {
         url
       },
       displayName: "renderFoo",
       location: {
         line: 10
       }
     };
 
     const component = mount(<Frame frame={frame} displayFullUrl={true} />);
-    expect(component.text()).toBe(`\trenderFoo ${url}:10\n`);
+    expect(component.text()).toBe(`renderFoo ${url}:10`);
   });
 
   it("getFrameTitle", () => {
     const url = `https://${"a".repeat(100)}.com/assets/src/js/foo-view.js`;
     const frame = {
       id: 1,
       source: {
         url
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
@@ -110,17 +110,17 @@ describe("Frames", () => {
       const component = mount(
         <Frames.WrappedComponent
           frames={frames}
           disableFrameTruncate={true}
           displayFullUrl={true}
         />
       );
       expect(component.text()).toBe(
-        "\trenderFoo http://myfile.com/mahscripts.js:55\n"
+        "renderFoo http://myfile.com/mahscripts.js:55"
       );
     });
 
     it("passes the getFrameTitle prop to the Frame component", () => {
       const frames = [
         {
           id: 1,
           displayName: "renderFoo",
@@ -260,10 +260,32 @@ describe("Frames", () => {
         }
       ];
       const selectedFrame = frames[0];
       const frameworkGroupingOn = true;
       const component = render({ frames, frameworkGroupingOn, selectedFrame });
 
       expect(component).toMatchSnapshot();
     });
+
+    it("selectable framework frames", () => {
+      const frames = [
+        { id: 1 },
+        { id: 2, library: "back" },
+        { id: 3, library: "back" },
+        { id: 8 }
+      ];
+
+      const selectedFrame = frames[0];
+
+      const component = render({
+        frames,
+        frameworkGroupingOn: false,
+        selectedFrame,
+        selectable: true
+      });
+      expect(component).toMatchSnapshot();
+
+      component.setProps({ frameworkGroupingOn: true });
+      expect(component).toMatchSnapshot();
+    });
   });
 });
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -1094,18 +1094,18 @@ const selectors = {
     remove: "#node-menu-delete-self",
     removeOthers: "#node-menu-delete-other",
     removeCondition: "#node-menu-remove-condition"
   },
   scopes: ".scopes-list",
   scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
   scopeValue: i =>
     `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`,
-  frame: i => `.frames ul li:nth-child(${i})`,
-  frames: ".frames ul li",
+  frame: i => `.frames [role="list"] [role="listitem"]:nth-child(${i})`,
+  frames: `.frames [role="list"] [role="listitem"]`,
   gutter: i => `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`,
   // These work for bobth the breakpoint listing and gutter marker
   gutterContextMenu: {
     addConditionalBreakpoint:
       "#node-menu-add-condition, #node-menu-add-conditional-breakpoint",
     editConditionalBreakpoint:
       "#node-menu-edit-condition, #node-menu-edit-conditional-breakpoint"
   },
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -1915,17 +1915,17 @@ function ErrorRep(props) {
   if (props.mode === MODE.TINY) {
     content.push(name);
   } else {
     content.push(`${name}: "${preview.message}"`);
   }
 
   if (preview.stack && props.mode !== MODE.TINY) {
     const stacktrace = props.renderStacktrace ? props.renderStacktrace(parseStackString(preview.stack)) : getStacktraceElements(props, preview);
-    content.push("\n", stacktrace);
+    content.push(stacktrace);
   }
 
   return span({
     "data-link-actor-id": object.actor,
     className: "objectBox-stackTrace"
   }, content);
 }