summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/test/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/test/chrome')
-rw-r--r--devtools/client/shared/components/test/chrome/accordion.snapshots.js176
-rw-r--r--devtools/client/shared/components/test/chrome/chrome.ini46
-rw-r--r--devtools/client/shared/components/test/chrome/head.js379
-rw-r--r--devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html209
-rw-r--r--devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html210
-rw-r--r--devtools/client/shared/components/test/chrome/test_HSplitBox_01.html140
-rw-r--r--devtools/client/shared/components/test/chrome/test_accordion.html141
-rw-r--r--devtools/client/shared/components/test/chrome/test_frame_01.html361
-rw-r--r--devtools/client/shared/components/test/chrome/test_frame_02.html103
-rw-r--r--devtools/client/shared/components/test/chrome/test_list.html127
-rw-r--r--devtools/client/shared/components/test/chrome/test_list_keyboard.html283
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_01.html136
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_02.html73
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_03.html87
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_04.html67
-rw-r--r--devtools/client/shared/components/test/chrome/test_notification_box_05.html63
-rw-r--r--devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html301
-rw-r--r--devtools/client/shared/components/test/chrome/test_searchbox.html74
-rw-r--r--devtools/client/shared/components/test/chrome/test_sidebar_toggle.html59
-rw-r--r--devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html141
-rw-r--r--devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html290
-rw-r--r--devtools/client/shared/components/test/chrome/test_smart-trace.html172
-rw-r--r--devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html98
-rw-r--r--devtools/client/shared/components/test/chrome/test_stack-trace.html100
-rw-r--r--devtools/client/shared/components/test/chrome/test_tabs_accessibility.html82
-rw-r--r--devtools/client/shared/components/test/chrome/test_tabs_menu.html84
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree-view_01.html290
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree-view_02.html136
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_01.html68
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_02.html49
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_03.html50
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_04.html133
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_05.html195
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_06.html340
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_07.html69
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_08.html61
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_09.html85
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_10.html57
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_11.html100
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_12.html146
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_13.html88
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_14.html245
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_15.html99
-rw-r--r--devtools/client/shared/components/test/chrome/test_tree_16.html145
44 files changed, 6358 insertions, 0 deletions
diff --git a/devtools/client/shared/components/test/chrome/accordion.snapshots.js b/devtools/client/shared/components/test/chrome/accordion.snapshots.js
new file mode 100644
index 0000000000..0f649b52d6
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/accordion.snapshots.js
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+window._snapshots = {
+ "Accordion basic render.": {
+ type: "ul",
+ props: {
+ className: "accordion",
+ tabIndex: -1,
+ },
+ children: [
+ {
+ type: "li",
+ props: {
+ id: "accordion-item-1",
+ className: "accordion-item",
+ "aria-labelledby": "accordion-item-1-header",
+ },
+ children: [
+ {
+ type: "h2",
+ props: {
+ id: "accordion-item-1-header",
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": false,
+ "aria-label": "Test Accordion Item 1",
+ onKeyDown: "event => this.onHeaderKeyDown(event, item)",
+ onClick: "event => this.onHeaderClick(event, item)",
+ },
+ children: [
+ {
+ type: "span",
+ props: {
+ className: "theme-twisty",
+ role: "presentation",
+ },
+ children: null,
+ },
+ {
+ type: "span",
+ props: { className: "accordion-header-label" },
+ children: ["Test Accordion Item 1"],
+ },
+ ],
+ },
+ {
+ type: "div",
+ props: {
+ className: "accordion-content",
+ hidden: true,
+ role: "presentation",
+ },
+ children: null,
+ },
+ ],
+ },
+ {
+ type: "li",
+ props: {
+ id: "accordion-item-2",
+ className: "accordion-item",
+ "aria-labelledby": "accordion-item-2-header",
+ },
+ children: [
+ {
+ type: "h2",
+ props: {
+ id: "accordion-item-2-header",
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": false,
+ "aria-label": "Test Accordion Item 2",
+ onKeyDown: "event => this.onHeaderKeyDown(event, item)",
+ onClick: "event => this.onHeaderClick(event, item)",
+ },
+ children: [
+ {
+ type: "span",
+ props: {
+ className: "theme-twisty",
+ role: "presentation",
+ },
+ children: null,
+ },
+ {
+ type: "span",
+ props: { className: "accordion-header-label" },
+ children: ["Test Accordion Item 2"],
+ },
+ {
+ type: "span",
+ props: {
+ className: "accordion-header-buttons",
+ role: "presentation",
+ },
+ children: [
+ {
+ type: "button",
+ props: {},
+ children: null,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "div",
+ props: {
+ className: "accordion-content",
+ hidden: true,
+ role: "presentation",
+ },
+ children: null,
+ },
+ ],
+ },
+ {
+ type: "li",
+ props: {
+ id: "accordion-item-3",
+ className: "accordion-item accordion-open",
+ "aria-labelledby": "accordion-item-3-header",
+ },
+ children: [
+ {
+ type: "h2",
+ props: {
+ id: "accordion-item-3-header",
+ className: "accordion-header",
+ tabIndex: 0,
+ "aria-expanded": true,
+ "aria-label": "Test Accordion Item 3",
+ onKeyDown: "event => this.onHeaderKeyDown(event, item)",
+ onClick: "event => this.onHeaderClick(event, item)",
+ },
+ children: [
+ {
+ type: "span",
+ props: {
+ className: "theme-twisty open",
+ role: "presentation",
+ },
+ children: null,
+ },
+ {
+ type: "span",
+ props: {
+ className: "accordion-header-label",
+ },
+ children: ["Test Accordion Item 3"],
+ },
+ ],
+ },
+ {
+ type: "div",
+ props: {
+ className: "accordion-content",
+ hidden: false,
+ role: "presentation",
+ },
+ children: [
+ {
+ type: "div",
+ props: {},
+ children: null,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+};
diff --git a/devtools/client/shared/components/test/chrome/chrome.ini b/devtools/client/shared/components/test/chrome/chrome.ini
new file mode 100644
index 0000000000..4d5a480ab7
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/chrome.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+support-files =
+ head.js
+ accordion.snapshots.js
+
+[test_accordion.html]
+[test_frame_01.html]
+[test_frame_02.html]
+[test_GridElementWidthResizer.html]
+[test_GridElementWidthResizer_RTL.html]
+[test_HSplitBox_01.html]
+[test_list.html]
+[test_list_keyboard.html]
+[test_notification_box_01.html]
+[test_notification_box_02.html]
+[test_notification_box_03.html]
+[test_notification_box_04.html]
+[test_notification_box_05.html]
+[test_searchbox.html]
+[test_searchbox-with-autocomplete.html]
+[test_sidebar_toggle.html]
+[test_smart-trace-grouping.html]
+[test_smart-trace-source-maps.html]
+[test_smart-trace.html]
+[test_stack-trace.html]
+[test_stack-trace-source-maps.html]
+[test_tabs_accessibility.html]
+[test_tabs_menu.html]
+[test_tree_01.html]
+[test_tree_02.html]
+[test_tree_03.html]
+[test_tree_04.html]
+[test_tree_05.html]
+[test_tree_06.html]
+[test_tree_07.html]
+[test_tree_08.html]
+[test_tree_09.html]
+[test_tree_10.html]
+[test_tree_11.html]
+[test_tree_12.html]
+[test_tree_13.html]
+[test_tree_14.html]
+[test_tree_15.html]
+[test_tree_16.html]
+[test_tree-view_01.html]
+[test_tree-view_02.html]
diff --git a/devtools/client/shared/components/test/chrome/head.js b/devtools/client/shared/components/test/chrome/head.js
new file mode 100644
index 0000000000..7abe54942f
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/head.js
@@ -0,0 +1,379 @@
+/* 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/. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+/* global _snapshots */
+
+"use strict";
+
+var { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+var { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+var { gDevTools } = require("resource://devtools/client/framework/devtools.js");
+var { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+var {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+var {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+var { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
+
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window,
+});
+
+const React = browserRequire("devtools/client/shared/vendor/react");
+const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+const dom = browserRequire("devtools/client/shared/vendor/react-dom-factories");
+const TestUtils = browserRequire(
+ "devtools/client/shared/vendor/react-dom-test-utils"
+);
+
+const ShallowRenderer = browserRequire(
+ "devtools/client/shared/vendor/react-test-renderer-shallow"
+);
+const TestRenderer = browserRequire(
+ "devtools/client/shared/vendor/react-test-renderer"
+);
+
+var EXAMPLE_URL = "https://example.com/browser/browser/devtools/shared/test/";
+
+SimpleTest.registerCleanupFunction(() => {
+ window._snapshots = null;
+});
+
+function forceRender(comp) {
+ return setState(comp, {}).then(() => setState(comp, {}));
+}
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+function onNextAnimationFrame(fn) {
+ return () => requestAnimationFrame(() => requestAnimationFrame(fn));
+}
+
+function setState(component, newState) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`SHARED-COMPONENTS-TEST: ${msg}\n`);
+}
+
+/**
+ * Tree View
+ */
+
+const TEST_TREE_VIEW = {
+ A: { label: "A", value: "A" },
+ B: { label: "B", value: "B" },
+ C: { label: "C", value: "C" },
+ D: { label: "D", value: "D" },
+ E: { label: "E", value: "E" },
+ F: { label: "F", value: "F" },
+ G: { label: "G", value: "G" },
+ H: { label: "H", value: "H" },
+ I: { label: "I", value: "I" },
+ J: { label: "J", value: "J" },
+ K: { label: "K", value: "K" },
+ L: { label: "L", value: "L" },
+};
+
+TEST_TREE_VIEW.children = {
+ A: [TEST_TREE_VIEW.B, TEST_TREE_VIEW.C, TEST_TREE_VIEW.D],
+ B: [TEST_TREE_VIEW.E, TEST_TREE_VIEW.F, TEST_TREE_VIEW.G],
+ C: [TEST_TREE_VIEW.H, TEST_TREE_VIEW.I],
+ D: [TEST_TREE_VIEW.J],
+ E: [TEST_TREE_VIEW.K, TEST_TREE_VIEW.L],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+};
+
+const TEST_TREE_VIEW_INTERFACE = {
+ provider: {
+ getChildren: x => TEST_TREE_VIEW.children[x.label],
+ hasChildren: x => !!TEST_TREE_VIEW.children[x.label].length,
+ getLabel: x => x.label,
+ getValue: x => x.value,
+ getKey: x => x.label,
+ getType: () => "string",
+ },
+ object: TEST_TREE_VIEW.A,
+ columns: [{ id: "default" }, { id: "value" }],
+};
+
+/**
+ * Tree
+ */
+
+var TEST_TREE_INTERFACE = {
+ getParent: x => TEST_TREE.parent[x],
+ getChildren: x => TEST_TREE.children[x],
+ renderItem: (x, depth, focused) =>
+ "-".repeat(depth) + x + ":" + focused + "\n",
+ getRoots: () => ["A", "M"],
+ getKey: x => "key-" + x,
+ itemHeight: 1,
+ onExpand: x => TEST_TREE.expanded.add(x),
+ onCollapse: x => TEST_TREE.expanded.delete(x),
+ isExpanded: x => TEST_TREE.expanded.has(x),
+};
+
+function isRenderedTree(actual, expectedDescription, msg) {
+ const expected = expectedDescription.map(x => x + "\n").join("");
+ dumpn(`Expected tree:\n${expected}`);
+ dumpn(`Actual tree:\n${actual}`);
+ is(actual, expected, msg);
+}
+
+function isAccessibleTree(tree, options = {}) {
+ const treeNode = tree.refs.tree;
+ is(treeNode.getAttribute("tabindex"), "0", "Tab index is set");
+ is(treeNode.getAttribute("role"), "tree", "Tree semantics is present");
+ if (options.hasActiveDescendant) {
+ ok(
+ treeNode.hasAttribute("aria-activedescendant"),
+ "Tree has an active descendant set"
+ );
+ }
+
+ const treeNodes = [...treeNode.querySelectorAll(".tree-node")];
+ for (const node of treeNodes) {
+ ok(node.id, "TreeNode has an id");
+ is(node.getAttribute("role"), "treeitem", "Tree item semantics is present");
+ is(
+ parseInt(node.getAttribute("aria-level"), 10),
+ parseInt(node.getAttribute("data-depth"), 10) + 1,
+ "Aria level attribute is set correctly"
+ );
+ }
+}
+
+// Encoding of the following tree/forest:
+//
+// A
+// |-- B
+// | |-- E
+// | | |-- K
+// | | `-- L
+// | |-- F
+// | `-- G
+// |-- C
+// | |-- H
+// | `-- I
+// `-- D
+// `-- J
+// M
+// `-- N
+// `-- O
+var TEST_TREE = {
+ children: {
+ A: ["B", "C", "D"],
+ B: ["E", "F", "G"],
+ C: ["H", "I"],
+ D: ["J"],
+ E: ["K", "L"],
+ F: [],
+ G: [],
+ H: [],
+ I: [],
+ J: [],
+ K: [],
+ L: [],
+ M: ["N"],
+ N: ["O"],
+ O: [],
+ },
+ parent: {
+ A: null,
+ B: "A",
+ C: "A",
+ D: "A",
+ E: "B",
+ F: "B",
+ G: "B",
+ H: "C",
+ I: "C",
+ J: "D",
+ K: "E",
+ L: "E",
+ M: null,
+ N: "M",
+ O: "N",
+ },
+ expanded: new Set(),
+};
+
+/**
+ * Frame
+ */
+function checkFrameString({
+ el,
+ file,
+ line,
+ column,
+ source,
+ functionName,
+ shouldLink,
+ tooltip,
+ locationPrefix,
+}) {
+ const $ = selector => el.querySelector(selector);
+
+ const $func = $(".frame-link-function-display-name");
+ const $source = $(".frame-link-source");
+ const $locationPrefix = $(".frame-link-prefix");
+ const $filename = $(".frame-link-filename");
+ const $line = $(".frame-link-line");
+
+ is($filename.textContent, file, "Correct filename");
+ is(
+ el.getAttribute("data-line"),
+ line ? `${line}` : null,
+ "Expected `data-line` found"
+ );
+ is(
+ el.getAttribute("data-column"),
+ column ? `${column}` : null,
+ "Expected `data-column` found"
+ );
+ is($source.getAttribute("title"), tooltip, "Correct tooltip");
+ is($source.tagName, shouldLink ? "A" : "SPAN", "Correct linkable status");
+ if (shouldLink) {
+ is($source.getAttribute("href"), source, "Correct source");
+ }
+
+ if (line != null) {
+ let lineText = `:${line}`;
+ if (column != null) {
+ lineText += `:${column}`;
+ }
+
+ is($line.textContent, lineText, "Correct line number");
+ } else {
+ ok(!$line, "Should not have an element for `line`");
+ }
+
+ if (functionName != null) {
+ is($func.textContent, functionName, "Correct function name");
+ } else {
+ ok(!$func, "Should not have an element for `functionName`");
+ }
+
+ if (locationPrefix != null) {
+ is($locationPrefix.textContent, locationPrefix, "Correct prefix");
+ } else {
+ ok(!$locationPrefix, "Should not have an element for `locationPrefix`");
+ }
+}
+
+function checkSmartFrameString({ el, location, functionName, tooltip }) {
+ const $ = selector => el.querySelector(selector);
+
+ const $func = $(".title");
+ const $location = $(".location");
+
+ is($location.textContent, location, "Correct filename");
+ is(el.getAttribute("title"), tooltip, "Correct tooltip");
+ if (functionName != null) {
+ is($func.textContent, functionName, "Correct function name");
+ } else {
+ ok(!$func, "Should not have an element for `functionName`");
+ }
+}
+
+function renderComponent(component, props) {
+ const el = React.createElement(component, props, {});
+ // By default, renderIntoDocument() won't work for stateless components, but
+ // it will work if the stateless component is wrapped in a stateful one.
+ // See https://github.com/facebook/react/issues/4839
+ const wrappedEl = dom.span({}, [el]);
+ const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
+ return ReactDOM.findDOMNode(renderedComponent).children[0];
+}
+
+function shallowRenderComponent(component, props) {
+ const el = React.createElement(component, props);
+ const renderer = new ShallowRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+/**
+ * Creates a React Component for testing
+ *
+ * @param {string} factory - factory object of the component to be created
+ * @param {object} props - React props for the component
+ * @returns {object} - container Node, Object with React component
+ * and querySelector function with $ as name.
+ */
+async function createComponentTest(factory, props) {
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+
+ const component = ReactDOM.render(factory(props), container);
+ await forceRender(component);
+
+ return {
+ container,
+ component,
+ $: s => container.querySelector(s),
+ };
+}
+
+async function waitFor(condition = () => true, delay = 50) {
+ do {
+ const res = condition();
+ if (res) {
+ return res;
+ }
+ await new Promise(resolve => setTimeout(resolve, delay));
+ } while (true);
+}
+
+/**
+ * Matches a component tree rendererd using TestRenderer to a given expected JSON
+ * snapshot.
+ * @param {String} name
+ * Name of the function derived from a test [step] name.
+ * @param {Object} el
+ * React element to be rendered using TestRenderer.
+ */
+function matchSnapshot(name, el) {
+ if (!_snapshots) {
+ is(false, "No snapshots were loaded into test.");
+ }
+
+ const snapshot = _snapshots[name];
+ if (snapshot === undefined) {
+ is(false, `Snapshot for "${name}" not found.`);
+ }
+
+ const renderer = TestRenderer.create(el, {});
+ const tree = renderer.toJSON();
+
+ is(
+ JSON.stringify(tree, (key, value) =>
+ typeof value === "function" ? value.toString() : value
+ ),
+ JSON.stringify(snapshot),
+ name
+ );
+}
diff --git a/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html
new file mode 100644
index 0000000000..cf30255141
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer.html
@@ -0,0 +1,209 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+ <!-- Basic tests for the GridElementWidthResizer component. -->
+ <head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link href="chrome://mochikit/content/tests/SimpleTest/test.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/skin/splitters.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/content/shared/components/splitter/GridElementResizer.css" rel="stylesheet" type="text/css"/>
+ <style>
+ * {
+ box-sizing: border-box;
+ }
+
+ html {
+ --theme-splitter-color: red;
+ }
+
+ main {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: 20px 20px 20px;
+ grid-gap: 10px;
+ }
+
+ .a,
+ .b,
+ .c,
+ .d {
+ border: 1px solid green;
+ }
+
+ header {
+ grid-column: 1 / -1;
+ }
+ .a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ min-width: 100px;
+ }
+ .b {
+ grid-column: 2 / 3;
+ grid-row: 2 / -1;
+ }
+
+ .c {
+ grid-column: 3 / 4;
+ grid-row: 2 / 3;
+ }
+
+ .d {
+ grid-column: 3 / 4;
+ grid-row: 3 / 4;
+ min-width: 150px;
+ }
+
+ .resizer-a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ }
+
+ .resizer-d {
+ grid-column: 3 / 4;
+ grid-row: 2 / -1;
+ }
+ </style>
+ </head>
+ <body>
+ <main></main>
+ <pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+const FUDGE_FACTOR = .5;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = async function () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const GridElementWidthResizer = React.createFactory(browserRequire("devtools/client/shared/components/splitter/GridElementWidthResizer"));
+ ok(GridElementWidthResizer, "Should get GridElementWidthResizer");
+
+ const resizedA = [];
+ const resizedD = [];
+
+ ReactDOM.render([
+ dom.header({}, "header"),
+ dom.aside({className: "a"}, "A"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => a,
+ enabled: true,
+ position: "end",
+ className: "resizer-a",
+ onResizeEnd: width => resizedA.push(width),
+ }),
+ dom.section({className: "b"}, "B"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => window.document.querySelector(".b"),
+ enabled: false,
+ position: "start",
+ className: "resizer-disabled",
+ }),
+ dom.aside({className: "c"}, "C"),
+ dom.aside({className: "d"}, "D"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => d,
+ enabled: true,
+ position: "start",
+ className: "resizer-d",
+ onResizeEnd: width => resizedD.push(width),
+ }),
+ ], window.document.querySelector("main"));
+
+ // wait for a bit as we may not have everything setup yet.
+ await new Promise(res => setTimeout(res, 10));
+
+ const a = window.document.querySelector(".a");
+ const d = window.document.querySelector(".d");
+
+ // Test that we properly rendered our two resizers, and not the disabled one.
+ const resizers = window.document.querySelectorAll(".grid-element-width-resizer");
+ is(resizers.length, 2, "The 2 enabled resizers are rendered");
+
+ const [resizerA, resizerD] = resizers;
+
+ ok(resizerA.classList.contains("resizer-a")
+ && resizerA.classList.contains("end"), "resizerA has expected classes");
+ ok(resizerD.classList.contains("resizer-d")
+ && resizerD.classList.contains("start"), "resizerD has expected classes");
+
+ const aBoundingRect = a.getBoundingClientRect();
+ const aOriginalWidth = aBoundingRect.width;
+
+ info("Resize element A");
+ await resize(resizerA, aBoundingRect.right + 20);
+
+ is(resizedA.length, 1, "onResizeEnd was called once");
+ is(resizedD.length, 0, "resizerD was not impacted");
+ let aWidth = a.getBoundingClientRect().width;
+ is(resizedA[0], aWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(aWidth, aOriginalWidth + 20),
+ "controlled element was resized to the expected size");
+
+ info("Resize element A below its min width");
+ await resize(resizerA, [aBoundingRect.left + 10]);
+ aWidth = a.getBoundingClientRect().width;
+ ok(aboutEq(aWidth, 100), "controlled element was resized to its min width");
+
+ info("Resize element D below its min width");
+ const dBoundingRect = d.getBoundingClientRect();
+ const dOriginalWidth = dBoundingRect.width;
+
+ await resize(resizerD, dBoundingRect.left + 100);
+ is(resizedD.length, 1, "onResizeEnd was called once for d");
+ is(resizedA.length, 2, "onResizeEnd wasn't called for a");
+ let dWidth = d.getBoundingClientRect().width;
+ is(resizedD[0], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, 150), "controlled element wasn't resized below its min-width");
+
+ info("Resize element D");
+ await resize(resizerD, dBoundingRect.left - 15);
+ dWidth = d.getBoundingClientRect().width;
+ is(resizedD[1], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, dOriginalWidth + 15), "element was resized");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ async function resize(resizer, clientX) {
+ info("Mouse down to start dragging");
+ synthesizeMouseAtCenter(resizer, { button: 0, type: "mousedown" }, window);
+ await SimpleTest.promiseWaitForCondition(
+ () => document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never set on the root element"
+ );
+ ok(true, "The dragging class is set on the root element");
+
+ const event = new MouseEvent("mousemove", { clientX });
+ resizer.dispatchEvent(event);
+
+ info("Mouse up to stop resizing");
+ synthesizeMouseAtCenter(document.body, { button: 0, type: "mouseup" }, window);
+
+ await SimpleTest.promiseWaitForCondition(
+ () => !document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never removed from the root element"
+ );
+ ok(true, "The dragging class is removed from the root element");
+ }
+};
+</script>
+</pre>
+ </body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html
new file mode 100644
index 0000000000..3768ecf0a0
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_GridElementWidthResizer_RTL.html
@@ -0,0 +1,210 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+ <!-- Basic tests for the GridElementWidthResizer component. -->
+ <head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link href="chrome://mochikit/content/tests/SimpleTest/test.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/skin/splitters.css" rel="stylesheet" type="text/css"/>
+ <link href="chrome://devtools/content/shared/components/splitter/GridElementResizer.css" rel="stylesheet" type="text/css"/>
+ <style>
+ * {
+ box-sizing: border-box;
+ }
+
+ html {
+ --theme-splitter-color: red;
+ }
+
+ main {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: 20px 20px 20px;
+ grid-gap: 10px;
+ direction: rtl;
+ }
+
+ .a,
+ .b,
+ .c,
+ .d {
+ border: 1px solid green;
+ }
+
+ header {
+ grid-column: 1 / -1;
+ }
+ .a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ min-width: 100px;
+ }
+ .b {
+ grid-column: 2 / 3;
+ grid-row: 2 / -1;
+ }
+
+ .c {
+ grid-column: 3 / 4;
+ grid-row: 2 / 3;
+ }
+
+ .d {
+ grid-column: 3 / 4;
+ grid-row: 3 / 4;
+ min-width: 150px;
+ }
+
+ .resizer-a {
+ grid-column: 1 / 2;
+ grid-row: 2 / -1;
+ }
+
+ .resizer-d {
+ grid-column: 3 / 4;
+ grid-row: 2 / -1;
+ }
+ </style>
+ </head>
+ <body>
+ <main></main>
+ <pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+const FUDGE_FACTOR = .5;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = async function () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+ const GridElementWidthResizer = React.createFactory(browserRequire("devtools/client/shared/components/splitter/GridElementWidthResizer"));
+ ok(GridElementWidthResizer, "Should get GridElementWidthResizer");
+
+ const resizedA = [];
+ const resizedD = [];
+
+ ReactDOM.render([
+ dom.header({}, "header"),
+ dom.aside({className: "a"}, "A"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => a,
+ enabled: true,
+ position: "end",
+ className: "resizer-a",
+ onResizeEnd: width => resizedA.push(width),
+ }),
+ dom.section({className: "b"}, "B"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => window.document.querySelector(".b"),
+ enabled: false,
+ position: "start",
+ className: "resizer-disabled",
+ }),
+ dom.aside({className: "c"}, "C"),
+ dom.aside({className: "d"}, "D"),
+ GridElementWidthResizer({
+ getControlledElementNode: () => d,
+ enabled: true,
+ position: "start",
+ className: "resizer-d",
+ onResizeEnd: width => resizedD.push(width),
+ }),
+ ], window.document.querySelector("main"));
+
+ // wait for a bit as we may not have everything setup yet.
+ await new Promise(res => setTimeout(res, 10));
+
+ const a = window.document.querySelector(".a");
+ const d = window.document.querySelector(".d");
+
+ // Test that we properly rendered our two resizers, and not the disabled one.
+ const resizers = window.document.querySelectorAll(".grid-element-width-resizer");
+ is(resizers.length, 2, "The 2 enabled resizers are rendered");
+
+ const [resizerA, resizerD] = resizers;
+
+ ok(resizerA.classList.contains("resizer-a")
+ && resizerA.classList.contains("end"), "resizerA has expected classes");
+ ok(resizerD.classList.contains("resizer-d")
+ && resizerD.classList.contains("start"), "resizerD has expected classes");
+
+ const aBoundingRect = a.getBoundingClientRect();
+ const aOriginalWidth = aBoundingRect.width;
+
+ info("Resize element A");
+ await resize(resizerA, aBoundingRect.left - 20);
+
+ is(resizedA.length, 1, "onResizeEnd was called once");
+ is(resizedD.length, 0, "resizerD was not impacted");
+ let aWidth = a.getBoundingClientRect().width;
+ is(resizedA[0], aWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(aWidth, aOriginalWidth + 20),
+ "controlled element was resized to the expected size");
+
+ info("Resize element A below its min width");
+ await resize(resizerA, [aBoundingRect.right - 10]);
+ aWidth = a.getBoundingClientRect().width;
+ ok(aboutEq(aWidth, 100), "controlled element was resized to its min width");
+
+ info("Resize element D below its min width");
+ const dBoundingRect = d.getBoundingClientRect();
+ const dOriginalWidth = dBoundingRect.width;
+
+ await resize(resizerD, dBoundingRect.right - 100);
+ is(resizedD.length, 1, "onResizeEnd was called once for d");
+ is(resizedA.length, 2, "onResizeEnd wasn't called for a");
+ let dWidth = d.getBoundingClientRect().width;
+ is(resizedD[0], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, 150), "controlled element wasn't resized below its min-width");
+
+ info("Resize element D");
+ await resize(resizerD, dBoundingRect.right + 15);
+ dWidth = d.getBoundingClientRect().width;
+ is(resizedD[1], dWidth, "onResizeEnd gives the width of the controlled element");
+ ok(aboutEq(dWidth, dOriginalWidth + 15), "element was resized");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ async function resize(resizer, clientX) {
+ info("Mouse down to start dragging");
+ synthesizeMouseAtCenter(resizer, { button: 0, type: "mousedown" }, window);
+ await SimpleTest.promiseWaitForCondition(
+ () => document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never set on the root element"
+ );
+ ok(true, "The dragging class is set on the root element");
+
+ const event = new MouseEvent("mousemove", { clientX });
+ resizer.dispatchEvent(event);
+
+ info("Mouse up to stop resizing");
+ synthesizeMouseAtCenter(document.body, { button: 0, type: "mouseup" }, window);
+
+ await SimpleTest.promiseWaitForCondition(
+ () => !document.firstElementChild.classList.contains("dragging"),
+ "dragging class is never removed from the root element"
+ );
+ ok(true, "The dragging class is removed from the root element");
+ }
+};
+</script>
+</pre>
+ </body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_HSplitBox_01.html b/devtools/client/shared/components/test/chrome/test_HSplitBox_01.html
new file mode 100644
index 0000000000..09da5dac6b
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_HSplitBox_01.html
@@ -0,0 +1,140 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Basic tests for the HSplitBox component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/splitters.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+ <style>
+ html {
+ --theme-splitter-color: black;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+const FUDGE_FACTOR = .1;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = async function () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ const HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/HSplitBox"));
+ ok(HSplitBox, "Should get HSplitBox");
+
+ const newSizes = [];
+
+ async function renderBox(props) {
+ const boxProps = Object.assign({
+ start: "hello!",
+ end: "world!",
+ startWidth: .5,
+ onResize(newSize) {
+ newSizes.push(newSize);
+ }
+ }, props);
+ const el = ReactDOM.render(HSplitBox(boxProps), window.document.body);
+ // wait until the element is rendered.
+ await SimpleTest.promiseWaitForCondition(
+ () => document.querySelector(".devtools-side-splitter")
+ );
+ return el;
+ }
+
+ await renderBox();
+
+ // Test that we properly rendered our two panes.
+
+ let panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should get two panes");
+ is(panes[0].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[1].style.flexGrow, "0.5", "Each pane should have .5 width");
+ is(panes[0].textContent.trim(), "hello!", "First pane should be hello");
+ is(panes[1].textContent.trim(), "world!", "Second pane should be world");
+
+ // Now change the left width and assert that the changes are reflected.
+
+ await renderBox({ startWidth: .25 });
+ panes = document.querySelectorAll(".h-split-box-pane");
+ is(panes.length, 2, "Should still have two panes");
+ is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");
+ is(panes[1].style.flexGrow, "0.75", "Second pane's width should be .75");
+
+ // Mouse moves without having grabbed the splitter should have no effect.
+
+ const container = document.querySelector(".h-split-box");
+ ok(container, "Should get our container .h-split-box");
+
+ const { left, top, width } = container.getBoundingClientRect();
+ const middle = left + width / 2;
+ const oneQuarter = left + width / 4;
+ const threeQuarters = left + 3 * width / 4;
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 0, "Mouse moves without dragging the splitter should have no effect");
+
+ // Send a mouse down on the splitter, and then move the mouse a couple
+ // times. Now we should get resizes.
+
+ const splitter = document.querySelector(".devtools-side-splitter");
+ ok(splitter, "Should get our splitter");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mousedown" }, window);
+
+ function mouseMove(clientX) {
+ const event = new MouseEvent("mousemove", { clientX });
+ document.defaultView.top.dispatchEvent(event);
+ }
+
+ mouseMove(middle);
+ is(newSizes.length, 1, "Should get 1 resize");
+ ok(aboutEq(newSizes[0], .5), "New size should be ~.5");
+
+ mouseMove(left);
+ is(newSizes.length, 2, "Should get 2 resizes");
+ ok(aboutEq(newSizes[1], 0), "New size should be ~0");
+
+ mouseMove(oneQuarter);
+ is(newSizes.length, 3, "Sould get 3 resizes");
+ ok(aboutEq(newSizes[2], .25), "New size should be ~.25");
+
+ mouseMove(threeQuarters);
+ is(newSizes.length, 4, "Should get 4 resizes");
+ ok(aboutEq(newSizes[3], .75), "New size should be ~.75");
+
+ synthesizeMouseAtCenter(splitter, { button: 0, type: "mouseup" }, window);
+
+ // Now that we have let go of the splitter, mouse moves should not result in resizes.
+
+ synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+ is(newSizes.length, 4, "Should still have 4 resizes");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_accordion.html b/devtools/client/shared/components/test/chrome/test_accordion.html
new file mode 100644
index 0000000000..60d179be6f
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_accordion.html
@@ -0,0 +1,141 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that Accordion renders correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Accordion component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="resource://testing-common/sinon-7.2.7.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script src="accordion.snapshots.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+/* global sinon */
+
+window.onload = async function() {
+ try {
+ const { button, div } = require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ renderIntoDocument,
+ findAllInRenderedTree,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Accordion =
+ browserRequire("devtools/client/shared/components/Accordion");
+
+ const testItems = [
+ {
+ header: "Test Accordion Item 1",
+ id: "accordion-item-1",
+ component: div({}),
+ opened: false,
+ onToggle: sinon.spy(),
+ },
+ {
+ header: "Test Accordion Item 2",
+ id: "accordion-item-2",
+ component: div({}),
+ buttons: button({}),
+ opened: false,
+ onToggle: sinon.spy(),
+ },
+ {
+ header: "Test Accordion Item 3",
+ id: "accordion-item-3",
+ component: div({}),
+ opened: true,
+ onToggle: sinon.spy(),
+ },
+ ];
+
+ // Accordion basic render
+ const accordion = React.createElement(Accordion, { items: testItems });
+
+ matchSnapshot("Accordion basic render.", accordion);
+
+ const tree = renderIntoDocument(accordion);
+ const headers = findAllInRenderedTree(tree, c => c.className === "accordion-header");
+
+ Simulate.click(headers[0]);
+ ok(testItems[0].onToggle.calledWith(true), "Handle toggling with click.");
+ ok(testItems[1].onToggle.notCalled,
+ "onToggle wasn't called on element we didn't click on.");
+
+ isDeeply(
+ tree.state,
+ {
+ everOpened: {
+ "accordion-item-1": true,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ opened: {
+ "accordion-item-1": true,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ },
+ "State updated correctly"
+ );
+
+ Simulate.keyDown(headers[0], { key: "Enter" });
+ ok(testItems[0].onToggle.calledWith(false), "Handle toggling with Enter key.");
+ isDeeply(
+ tree.state,
+ {
+ everOpened: {
+ "accordion-item-1": true,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ opened: {
+ "accordion-item-1": false,
+ "accordion-item-2": false,
+ "accordion-item-3": true,
+ },
+ },
+ "State updated correctly"
+ );
+
+ Simulate.keyDown(headers[1], { key: " " });
+ ok(testItems[1].onToggle.calledWith(true), "Handle toggling with Space key.");
+ isDeeply(
+ tree.state,
+ {
+ everOpened: {
+ "accordion-item-1": true,
+ "accordion-item-2": true,
+ "accordion-item-3": true,
+ },
+ opened: {
+ "accordion-item-1": false,
+ "accordion-item-2": true,
+ "accordion-item-3": true,
+ },
+ },
+ "State updated correctly"
+ );
+
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_frame_01.html b/devtools/client/shared/components/test/chrome/test_frame_01.html
new file mode 100644
index 0000000000..f763b21395
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_frame_01.html
@@ -0,0 +1,361 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html>
+ <!--
+Test the formatting of the file name, line and columns are correct in frame components,
+with optional columns, unknown and non-URL sources.
+-->
+ <head>
+ <meta charset="utf-8" />
+ <title>Frame component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link
+ rel="stylesheet"
+ type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ />
+ </head>
+ <body>
+ <pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Frame = React.createFactory(browserRequire("devtools/client/shared/components/Frame"));
+ ok(Frame, "Should get Frame");
+
+ // Check when there's a column
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check when there's no column
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when column === 0
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 0,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when there's an error in CSS (View source in Style Editor)
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/cafebabe.css",
+ line: 13,
+ },
+ messageSource: "css",
+ }, {
+ file: "cafebabe.css",
+ line: 13,
+ shouldLink: true,
+ tooltip: "View source in Style Editor → https://myfile.com/cafebabe.css:13",
+ });
+
+
+ // Check when there's no parseable URL source;
+ // should not link but should render line/columns
+ await checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ shouldLink: false,
+ tooltip: "self-hosted:1",
+ });
+ await checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "self-hosted:1:10",
+ });
+
+ // Check when there's no source;
+ // should not link but should render line/columns
+ await checkFrameComponent({
+ frame: {
+ line: 1,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ shouldLink: false,
+ tooltip: "(unknown):1",
+ });
+ await checkFrameComponent({
+ frame: {
+ line: 1,
+ column: 10,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ column: "10",
+ shouldLink: false,
+ tooltip: "(unknown):1:10",
+ });
+
+ // Check when there's a column, but no line;
+ // no line/column info should render
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check when line is 0; this should be an invalid
+ // line option, so don't render line/column
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that line and column can be strings
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: "10",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 10,
+ column: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:10:55",
+ });
+
+ // Check that line and column can be strings,
+ // and that the `0` rendering rules apply when they are strings as well
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: "0",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFullSourceUrl option works correctly
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFullSourceUrl: true
+ }, {
+ file: "https://myfile.com/mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFunctionName option works correctly
+ await checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ }
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ await checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: "myfun",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check that anonymous function name is not displayed unless explicitly enabled
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true
+ }, {
+ functionName: "<anonymous>",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js",
+ });
+
+ // Check if file is rendered with "/" for root documents when showEmptyPathAsHost is false
+ await checkFrameComponent({
+ frame: {
+ source: "https://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: false,
+ }, {
+ file: "/",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://www.cnn.com/:1",
+ });
+
+ // Check if file is rendered with hostname for root documents when showEmptyPathAsHost is true
+ await checkFrameComponent({
+ frame: {
+ source: "https://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: true,
+ }, {
+ file: "www.cnn.com",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://www.cnn.com/:1",
+ });
+
+ const resolvedLocation = {
+ sourceId: "whatever",
+ line: 23,
+ sourceUrl: "https://bugzilla.mozilla.org/original.js",
+ };
+ const mockSourceMapURLService = {
+ subscribeByLocation (loc, callback) {
+ // Resolve immediately.
+ callback({
+ url: resolvedLocation.sourceUrl,
+ line: resolvedLocation.line,
+ column: undefined,
+ });
+ return () => {};
+ },
+ };
+ await checkFrameComponent({
+ frame: {
+ line: 97,
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ },
+ sourceMapURLService: mockSourceMapURLService,
+ }, {
+ file: "original.js",
+ line: resolvedLocation.line,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:23",
+ source: "https://bugzilla.mozilla.org/original.js",
+ });
+
+ // Check when a message comes from a logPoint,
+ // a prefix should render before source
+ await checkFrameComponent({
+ frame: {
+ source: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ options: { logPoint: true },
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55:10",
+ });
+
+ function checkFrameComponent(input, expected) {
+ const props = Object.assign({ onClick: () => {} }, input);
+ const frame = ReactDOM.render(Frame(props), window.document.body);
+ const el = ReactDOM.findDOMNode(frame);
+ const { source } = input.frame;
+ checkFrameString(Object.assign({ el, source }, expected));
+ ReactDOM.unmountComponentAtNode(window.document.body);
+ }
+
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+ </body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_frame_02.html b/devtools/client/shared/components/test/chrome/test_frame_02.html
new file mode 100644
index 0000000000..dd4bf9c2b7
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_frame_02.html
@@ -0,0 +1,103 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the frame component reacts to source-map pref changse.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Frame component source-map test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Frame = React.createFactory(browserRequire("devtools/client/shared/components/Frame"));
+
+ const resolvedLocation = {
+ sourceId: "whatever",
+ line: 23,
+ sourceUrl: "https://bugzilla.mozilla.org/original.js",
+ };
+ const mockSourceMapURLService = {
+ _update () {
+ this._callback(Services.prefs.getBoolPref(PREF)
+ ? {
+ url: resolvedLocation.sourceUrl,
+ line: resolvedLocation.line,
+ column: undefined,
+ }
+ : null);
+ },
+ subscribeByLocation (loc, callback) {
+ this._callback = callback;
+ // Resolve immediately.
+ this._update();
+
+ return () => {};
+ },
+ };
+
+ const props = {
+ onClick: () => {},
+ frame: {
+ line: 97,
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ },
+ sourceMapURLService: mockSourceMapURLService,
+ };
+
+ const PREF = "devtools.source-map.client-service.enabled";
+ Services.prefs.setBoolPref(PREF, false);
+
+ const frame = ReactDOM.render(Frame(props), window.document.body);
+ const el = ReactDOM.findDOMNode(frame);
+ const { source } = props.frame;
+
+ const expectedOriginal = {
+ file: "original.js",
+ line: resolvedLocation.line,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:23",
+ source: "https://bugzilla.mozilla.org/original.js",
+ };
+ const expectedGenerated = {
+ file: "bundle.js",
+ line: 97,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/bundle.js:97",
+ source: "https://bugzilla.mozilla.org/bundle.js",
+ };
+
+ checkFrameString(Object.assign({ el, source }, expectedGenerated));
+
+ Services.prefs.setBoolPref(PREF, true);
+ mockSourceMapURLService._update();
+ checkFrameString(Object.assign({ el, source }, expectedOriginal));
+
+ Services.prefs.setBoolPref(PREF, false);
+ mockSourceMapURLService._update();
+ checkFrameString(Object.assign({ el, source }, expectedGenerated));
+
+ Services.prefs.clearUserPref(PREF);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_list.html b/devtools/client/shared/components/test/chrome/test_list.html
new file mode 100644
index 0000000000..8be8907b5d
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_list.html
@@ -0,0 +1,127 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that List renders correctly.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>List component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script src="list.snapshots.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function() {
+ try {
+ const { div } = require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ renderIntoDocument,
+ findRenderedDOMComponentWithClass,
+ scryRenderedDOMComponentsWithTag,
+ scryRenderedComponentsWithType,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const { List, ListItem } =
+ browserRequire("devtools/client/shared/components/List");
+
+ const testItems = [
+ {
+ component: div({ className: "item-1" }, "Test List Item 1"),
+ className: "list-item-1",
+ key: "list-item-1",
+ },
+ {
+ component: div({ className: "item-2" }, "Test List Item 2"),
+ className: "list-item-2",
+ key: "list-item-2",
+ },
+ {
+ component: div({ className: "item-3" }, "Test List Item 3"),
+ className: "list-item-3",
+ key: "list-item-3",
+ },
+ ];
+
+ const listReactEl = React.createElement(List, {
+ items: testItems,
+ labelledBy: "test-labelledby",
+ });
+
+ const list = renderIntoDocument(listReactEl);
+ const listEl = findRenderedDOMComponentWithClass(list, "list");
+ const items = scryRenderedComponentsWithType(list, ListItem);
+ const itemEls = scryRenderedDOMComponentsWithTag(list, "li");
+
+ function testCurrent(index) {
+ is(list.state.current, index, "Correct current item.");
+ is(listEl.getAttribute("aria-activedescendant"), testItems[index].key,
+ "Correct active descendant.");
+ }
+
+ is(items.length, 3, "Correct number of list item components in tree.");
+ is(itemEls.length, 3, "Correct number of list items is rendered.");
+ info("Testing initial tree properties.");
+ for (let index = 0; index < items.length; index++) {
+ const item = items[index];
+ const itemEl = itemEls[index];
+ const { active, current, item: itemProp } = item.props;
+ const content = itemEl.querySelector(".list-item-content");
+
+ is(active, false, "Correct active state.");
+ is(current, false, "Correct current state.");
+ is(itemProp, testItems[index], "Correct rendered item.");
+ is(item.contentRef.current, content, "Correct content ref.");
+
+ is(itemEl.className, testItems[index].className, "Correct list item class.");
+ is(itemEl.id, testItems[index].key, "Correct list item it.");
+ is(content.getAttribute("role"), "presentation", "Correct content role.");
+
+ is(content.innerHTML,
+ `<div class="item-${index + 1}">Test List Item ${index + 1}</div>`,
+ "Content rendered correctly.");
+ }
+
+ is(list.state.current, null, "Current item is not set by default.");
+ is(list.state.active, null, "Active item is not set by default.");
+ is(list.listRef.current, listEl, "Correct list ref.");
+
+ is(listEl.className, "list", "Correct list class.");
+ is(listEl.tabIndex, 0, "List is focusable.");
+ ok(!listEl.hasAttribute("aria-label"), "List has no label.");
+ is(listEl.getAttribute("aria-labelledby"), "test-labelledby",
+ "Correct list labelled by attribute.");
+ ok(!listEl.hasAttribute("aria-activedescendant"),
+ "No active descendant set by default.");
+
+ Simulate.focus(listEl);
+ testCurrent(0);
+
+ Simulate.click(itemEls[2]);
+ testCurrent(2);
+
+ Simulate.blur(listEl);
+ testCurrent(2);
+
+ Simulate.focus(listEl);
+ testCurrent(2);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_list_keyboard.html b/devtools/client/shared/components/test/chrome/test_list_keyboard.html
new file mode 100644
index 0000000000..7558404ed2
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_list_keyboard.html
@@ -0,0 +1,283 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that List component has working keyboard interactions.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>List component keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = function() {
+ try {
+ const { a, button, div } =
+ require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ findRenderedDOMComponentWithTag,
+ scryRenderedDOMComponentsWithTag,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const { List } =
+ browserRequire("devtools/client/shared/components/List");
+
+ const testItems = [
+ {
+ component: div({}, "Test List Item 1"),
+ className: "list-item-1",
+ key: "list-item-1",
+ },
+ {
+ component: div({},
+ "Test List Item 2",
+ a({ href: "#" }, "Focusable 1"),
+ button({ }, "Focusable 2")),
+ className: "list-item-2",
+ key: "list-item-2",
+ },
+ {
+ component: div({}, "Test List Item 3"),
+ className: "list-item-3",
+ key: "list-item-3",
+ },
+ ];
+
+ const list = React.createElement(List, {
+ items: testItems,
+ labelledby: "test-labelledby",
+ });
+
+ const tree = ReactDOM.render(list, document.body);
+ const listEl = findRenderedDOMComponentWithClass(tree, "list");
+ scryRenderedDOMComponentsWithTag(tree, "li");
+ const defaultFocus = listEl.ownerDocument.body;
+
+ function blurEl(el) {
+ // Simulate.blur does not seem to update the activeElement.
+ el.blur();
+ }
+
+ function focusEl(el) {
+ // Simulate.focus does not seem to update the activeElement.
+ el.focus();
+ }
+
+ const tests = [{
+ name: "Test default List state. Keyboard focus is set to document body by default.",
+ state: { current: null, active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Current item must be set to the first list item on initial focus. " +
+ "Keyboard focus should be set on list's conatiner (<ul>).",
+ action: () => focusEl(listEl),
+ activeElement: listEl,
+ state: { current: 0 },
+ }, {
+ name: "Current item should remain set even when the list is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => blurEl(listEl),
+ state: { current: 0 },
+ activeElement: defaultFocus,
+ }, {
+ name: "Unset list's current state.",
+ action: () => tree.setState({ current: null }),
+ state: { current: null },
+ }, {
+ name: "Current item must be re-set again to the first list item on initial " +
+ "focus. Keyboard focus should be set on list's conatiner (<ul>).",
+ action: () => focusEl(listEl),
+ activeElement: listEl,
+ state: { current: 0 },
+ }, {
+ name: "Current item should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 1 },
+ }, {
+ name: "Current item should be updated to last on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 2 },
+ }, {
+ name: "Current item should remain on last on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 2 },
+ }, {
+ name: "Current item should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 1 },
+ }, {
+ name: "Current item should be updated to first on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 0 },
+ }, {
+ name: "Current item should remain on first on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 0 },
+ }, {
+ name: "Current item should be updated to last on End.",
+ event: { type: "keyDown", el: listEl, options: { key: "End" }},
+ state: { current: 2 },
+ }, {
+ name: "Current item should be updated to first on Home.",
+ event: { type: "keyDown", el: listEl, options: { key: "Home" }},
+ state: { current: 0 },
+ }, {
+ name: "Current item should be set as active on Enter.",
+ event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
+ state: { current: 0, active: 0 },
+ activeElement: listEl,
+ }, {
+ name: "Active item should be unset on Escape.",
+ event: { type: "keyDown", el: listEl, options: { key: "Escape" }},
+ state: { current: 0, active: null },
+ }, {
+ name: "Current item should be set as active on Space.",
+ event: { type: "keyDown", el: listEl, options: { key: " " }},
+ state: { current: 0, active: 0 },
+ activeElement: listEl,
+ }, {
+ name: "Current item should unset when focus leaves the list.",
+ action: () => blurEl(listEl),
+ state: { current: 0, active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.",
+ action: () => focusEl(listEl),
+ activeElement: listEl,
+ }, {
+ name: "Current item should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }},
+ state: { current: 1, active: null },
+ }, {
+ name: "Current item should be set as active on Enter. Keyboard focus should be " +
+ "set on the first focusable element inside the list item, if available.",
+ event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ // When list item becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should be set to next tabbable element inside the active " +
+ "list item on Tab.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ // When list item becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the list item when focused on last " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the list item when focused on first " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Active item should be unset on Escape. Focus should move back to the " +
+ "list container.",
+ event: { type: "keyDown", el: listEl, options: { key: "Escape" }},
+ state: { current: 1, active: null },
+ activeElement: listEl,
+ }, {
+ name: "Current item should be set as active on Space. Keyboard focus should be " +
+ "set on the first focusable element inside the list item, if available.",
+ event: { type: "keyDown", el: listEl, options: { key: " " }},
+ state: { current: 1, active: 1 },
+ get activeElement() {
+ // When list item becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Current item should remain set even when the list is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => listEl.ownerDocument.activeElement.blur(),
+ state: { current: 1, active: null, },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.",
+ action: () => focusEl(listEl),
+ state: { current: 1, active: null },
+ activeElement: listEl,
+ }, {
+ name: "Current item should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }},
+ state: { current: 0, active: null },
+ }, {
+ name: "Current item should be set as active on Enter.",
+ event: { type: "keyDown", el: listEl, options: { key: "Enter" }},
+ state: { current: 0, active: 0 },
+ activeElement: listEl,
+ }, {
+ name: "Keyboard focus should move to another focusable element outside of the " +
+ "list when there's nothing to focus on inside the list item.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { current: 0, active: null },
+ activeElement: listEl.ownerDocument.documentElement,
+ }];
+
+ for (const test of tests) {
+ const { action, event, state, name } = test;
+
+ is(listEl, findRenderedDOMComponentWithClass(tree, "list"), "Sanity check");
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ Simulate[type](el, options);
+ } else if (action) {
+ action();
+ }
+
+ if (test.activeElement) {
+ is(listEl.ownerDocument.activeElement, test.activeElement,
+ "Focus is set correctly.");
+ }
+
+ for (const key in state) {
+ is(tree.state[key], state[key], `${key} state is correct.`);
+ }
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_01.html b/devtools/client/shared/components/test/chrome/test_notification_box_01.html
new file mode 100644
index 0000000000..2921d607c3
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_01.html
@@ -0,0 +1,136 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Basic rendering
+* Appending correct classname on wrapping
+* Appending a notification
+* Notification priority
+* Closing notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ const renderedBox = shallowRenderComponent(NotificationBox, {});
+ is(renderedBox.type, "div", "NotificationBox is rendered as <div>");
+
+ info("Test rendering NotificationBox with default props");
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ ok(notificationNode.classList.contains("notificationbox"),
+ "NotificationBox has expected class");
+ ok(notificationNode.classList.contains("border-bottom"),
+ "NotificationBox has expected class");
+ is(notificationNode.textContent, "",
+ "Empty NotificationBox has no text content");
+
+ checkNumberOfNotifications(notificationBox, 0);
+
+ // Append a notification
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_HIGH
+ );
+
+ is (notificationNode.textContent, "Info message",
+ "The box must display notification message");
+ checkNumberOfNotifications(notificationBox, 1);
+
+ // Append more important notification
+ notificationBox.appendNotification(
+ "Critical message",
+ "id2",
+ null,
+ PriorityLevels.PRIORITY_CRITICAL_BLOCK
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must display more important notification message");
+
+ // Append less important notification
+ notificationBox.appendNotification(
+ "Warning message",
+ "id3",
+ null,
+ PriorityLevels.PRIORITY_WARNING_HIGH
+ );
+
+ checkNumberOfNotifications(notificationBox, 1);
+
+ is (notificationNode.textContent, "Critical message",
+ "The box must still display the more important notification");
+
+ ok(notificationBox.getCurrentNotification(),
+ "There must be current notification");
+
+ notificationBox.getNotificationWithValue("id1").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id2").close();
+ checkNumberOfNotifications(notificationBox, 1);
+
+ notificationBox.getNotificationWithValue("id3").close();
+ checkNumberOfNotifications(notificationBox, 0);
+
+ info(`Check "wrapping" prop works as expected`);
+ // Append wrapping classname to the dom element when passing wrapping prop
+ const boxElementWrapped = React.createElement(NotificationBox, {wrapping: true});
+ const notificationBoxWrapped = TestUtils.renderIntoDocument(boxElementWrapped);
+ const wrappedNotificationNode = ReactDOM.findDOMNode(notificationBoxWrapped);
+
+ ok(wrappedNotificationNode.classList.contains("wrapping"),
+ "Wrapped notificationBox has expected class");
+
+ info(`Check "displayBorderTop/displayBorderBottom" props work as expected`);
+ const element = React.createElement(NotificationBox, {
+ displayBorderTop: true,
+ displayBorderBottom: false,
+ });
+ const renderedElement = TestUtils.renderIntoDocument(element);
+ const elementNode = ReactDOM.findDOMNode(renderedElement);
+
+ ok(elementNode.classList.contains("border-top"),
+ "truthy displayBorderTop render a border-top className");
+ ok(!elementNode.classList.contains("border-bottom"),
+ "falsy displayBorderBottom does not render a border-bottom className");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+
+function checkNumberOfNotifications(notificationBox, expected) {
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, expected,
+ "The notification box must have expected number of notifications");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_02.html b/devtools/client/shared/components/test/chrome/test_notification_box_02.html
new file mode 100644
index 0000000000..f74194d128
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_02.html
@@ -0,0 +1,73 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom callback in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Test rendering
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let callbackExecuted = false;
+
+ // Append a notification.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ undefined,
+ (reason) => {
+ callbackExecuted = true;
+ is(reason, "removed", "The reason must be expected string");
+ }
+ );
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ const closeButton = notificationNode.querySelector(
+ ".messageCloseButton");
+
+ // Click the close button to close the notification.
+ TestUtils.Simulate.click(closeButton);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+
+ ok(callbackExecuted, "Event callback must be executed.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_03.html b/devtools/client/shared/components/test/chrome/test_notification_box_03.html
new file mode 100644
index 0000000000..816456edd3
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_03.html
@@ -0,0 +1,87 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom buttons in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Test rendering
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let buttonCallbackExecuted = false;
+ const buttons = [{
+ label: "Button1",
+ callback: () => {
+ buttonCallbackExecuted = true;
+
+ // Do not close the notification
+ return true;
+ },
+ }, {
+ label: "Button2",
+ callback: () => {
+ // Close the notification (return value undefined)
+ },
+ }];
+
+ // Append a notification with buttons.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ buttons
+ );
+
+ const buttonNodes = notificationNode.querySelectorAll(
+ ".notificationButton");
+
+ is(buttonNodes.length, 2, "There must be two buttons");
+
+ // Click the first button
+ TestUtils.Simulate.click(buttonNodes[0]);
+ ok(buttonCallbackExecuted, "Button callback must be executed.");
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 1,
+ "There must be one notification");
+
+ // Click the second button (closing the notification)
+ TestUtils.Simulate.click(buttonNodes[1]);
+
+ is(TestUtils.scryRenderedDOMComponentsWithClass(
+ notificationBox, "notification").length, 0,
+ "The notification box must be empty now");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_04.html b/devtools/client/shared/components/test/chrome/test_notification_box_04.html
new file mode 100644
index 0000000000..07ad9af25c
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_04.html
@@ -0,0 +1,67 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* Adding a mdnLink to a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Render notification
+ const boxElement = React.createElement(NotificationBox);
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ const mdnLink = "https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors"
+
+ const mdnLinkButton = {mdnUrl: mdnLink, label: "learn more about error" }
+
+ // Append a notification with a learn-more link
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ [mdnLinkButton],
+ (e) => false,
+ );
+
+ const linkNode = notificationNode.querySelector(
+ "a.learn-more-link");
+
+ ok(linkNode, "Link is present");
+
+ is(linkNode.title, "learn more about error", "link has correct title");
+ ok(linkNode.classList.contains("devtools-button"), "link has correct class")
+
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_notification_box_05.html b/devtools/client/shared/components/test/chrome/test_notification_box_05.html
new file mode 100644
index 0000000000..b3a4e96378
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_notification_box_05.html
@@ -0,0 +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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test for Notification Box. The test is checking:
+* the close button is not present when displayCloseButton is false
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/NotificationBox");
+
+ // Test rendering with close button disabled
+ const boxElement = React.createElement(NotificationBox, {displayCloseButton: false});
+ const notificationBox = TestUtils.renderIntoDocument(boxElement);
+ const notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+
+
+ // Append a notification.
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ PriorityLevels.PRIORITY_INFO_LOW,
+ [],
+ (e) => false,
+ );
+
+ // Ensure close button is not present
+ const linkNode = notificationNode.querySelector(
+ ".messageCloseButton");
+
+ ok(!linkNode, "Close button is not present");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html b/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html
new file mode 100644
index 0000000000..110d5640c1
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_searchbox-with-autocomplete.html
@@ -0,0 +1,301 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html>
+<!--
+Test the searchbox and autocomplete-popup components
+-->
+<head>
+ <meta charset="utf-8">
+ <title>SearchBox component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+window.onload = async function () {
+ /**
+ * Takes a DOMNode with its children as list items,
+ * Typically UL > LI and each item's text value is
+ * compared with the reference item's value as a test
+ *
+ * @params {Node} - Node to be compared
+ * @reference {array} - Reference array for comparison. The selected index is
+ * highlighted as a single element array ie. ["[abc]", "ab", "abcPQR"],
+ * Here the element "abc" is highlighted
+ */
+ function compareAutocompleteList(list, reference) {
+ const delimiter = " - ";
+ const observedList = [...list.children].map(el => {
+ return el.classList.contains("autocomplete-selected")
+ ? `[${el.textContent}]`
+ : el.textContent
+ });
+ is(observedList.join(delimiter), reference.join(delimiter),
+ "Autocomplete items are rendered as expected");
+ }
+
+ function compareCursorPosition(initialElement) {
+ const initialPosition = initialElement.selectionStart;
+ return (element) => {
+ is(element.selectionStart, initialPosition, "Input cursor position is not changed");
+ }
+ }
+
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SearchBox = React.createFactory(
+ browserRequire("devtools/client/shared/components/SearchBox")
+ );
+ const { component, $ } = await createComponentTest(SearchBox, {
+ type: "search",
+ autocompleteProvider: (filter) => {
+ const baseList = [
+ "foo",
+ "BAR",
+ "baZ",
+ "abc",
+ "pqr",
+ "xyz",
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ ];
+ if (!filter) {
+ return [];
+ }
+
+ const tokens = filter.split(/\s+/g);
+ const lastToken = tokens[tokens.length - 1];
+ const previousTokens = tokens.slice(0, tokens.length - 1);
+
+ if (!lastToken) {
+ return [];
+ }
+
+ return baseList
+ .filter((item) => {
+ return item.toLowerCase().startsWith(lastToken.toLowerCase())
+ && item.toLowerCase() !== lastToken.toLowerCase();
+ })
+ .sort()
+ .map(item => ({
+ value: [...previousTokens, item].join(" "),
+ displayValue: item,
+ }));
+ },
+ onChange: () => null,
+ });
+
+ async function testSearchBoxWithAutocomplete() {
+ ok(!$(".devtools-autocomplete-popup"), "Autocomplete list not visible");
+
+ $(".devtools-searchinput").focus();
+ await forceRender(component); // Wait for state update
+ ok(!$(".devtools-autocomplete-popup"), "Autocomplete list not visible");
+
+ sendString("a");
+ await forceRender(component);
+
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+
+ // Blur event
+ $(".devtools-searchinput").blur();
+ await forceRender(component);
+ ok(!component.state.focused, "focused state was properly set");
+ ok(!$(".devtools-autocomplete-popup"), "Autocomplete list removed from DOM");
+ }
+
+ async function testKeyEventsWithAutocomplete() {
+ // Clear the initial input
+ $(".devtools-searchinput").focus();
+ const cursorPositionIsNotChanged = compareCursorPosition($(".devtools-searchinput"));
+
+ // ArrowDown
+ synthesizeKey("KEY_ArrowDown");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "[a1]",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+ ok($(".devtools-autocomplete-listbox .autocomplete-item:nth-child(2)")
+ .className.includes("autocomplete-selected"),
+ "Selection class applied");
+
+ // A double ArrowUp should roll back to the bottom of the list
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowUp");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "[abc]",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // PageDown should take -5 places up
+ synthesizeKey("KEY_PageUp");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "[a1]",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // PageDown should take +5 places down
+ synthesizeKey("KEY_PageDown");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "[abc]",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // Home should take to the top of the list
+ synthesizeKey("KEY_Home");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "abc",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // End should take to the bottom of the list
+ synthesizeKey("KEY_End");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "ABC",
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "[abc]",
+ ]);
+ cursorPositionIsNotChanged($(".devtools-searchinput"));
+
+ // Key down in existing state should rollover to the top
+ synthesizeKey("KEY_ArrowDown");
+ await forceRender(component);
+ // Tab should select the component and hide popup
+ synthesizeKey("KEY_Tab");
+ await forceRender(component);
+ is(component.state.value, "ABC", "Tab hit selects the item");
+ ok(!$(".devtools-autocomplete-popup"), "Tab hit hides the popup");
+
+ // Activate popup by removing a key
+ synthesizeKey("KEY_Backspace");
+ await forceRender(component);
+ ok($(".devtools-autocomplete-popup"), "Popup is up");
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "abc"
+ ]);
+
+ // Enter key selection
+ synthesizeKey("KEY_ArrowUp");
+ await forceRender(component);
+ synthesizeKey("KEY_Enter");
+ is(component.state.value, "abc", "Enter selection");
+ ok(!$(".devtools-autocomplete-popup"), "Enter/Return hides the popup");
+
+ // Escape should remove the autocomplete component
+ synthesizeKey("KEY_Backspace");
+ await forceRender(component);
+ synthesizeKey("KEY_Escape");
+ await forceRender(component);
+ ok(!$(".devtools-autocomplete-popup"),
+ "Autocomplete list removed from DOM on Escape");
+ }
+
+ async function testMouseEventsWithAutocomplete() {
+ $(".devtools-searchinput").focus();
+ await setState(component, {
+ value: "",
+ focused: true,
+ });
+ await forceRender(component);
+
+ // ArrowDown
+ synthesizeKey("KEY_ArrowDown");
+ await forceRender(component);
+ synthesizeMouseAtCenter($(".devtools-searchinput"), {}, window);
+ await forceRender(component);
+ is(component.state.focused, true, "Component should now be focused");
+
+ sendString("pq");
+ await forceRender(component);
+ synthesizeMouseAtCenter(
+ $(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
+ {}, window
+ );
+ await forceRender(component);
+ is(component.state.value, "pqr", "Mouse click selects the item.");
+ ok(!$(".devtools-autocomplete-popup"), "Mouse click on item hides the popup");
+ }
+
+ async function testTokenizedAutocomplete() {
+ // Test for string "pqr ab" which should show list of ABC, abc
+ sendString(" ab");
+ await forceRender(component);
+ compareAutocompleteList($(".devtools-autocomplete-listbox"), [
+ "[ABC]",
+ "abc"
+ ]);
+
+ // Select the first element, value now should be "pqr ABC"
+ synthesizeMouseAtCenter(
+ $(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
+ {}, window
+ );
+ is(component.state.value, "pqr ABC", "Post Tokenization value selection");
+ }
+
+ add_task(async function () {
+ await testSearchBoxWithAutocomplete();
+ await testKeyEventsWithAutocomplete();
+ await testMouseEventsWithAutocomplete();
+ await testTokenizedAutocomplete();
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_searchbox.html b/devtools/client/shared/components/test/chrome/test_searchbox.html
new file mode 100644
index 0000000000..8e2f76c1b9
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_searchbox.html
@@ -0,0 +1,74 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html>
+<!--
+Test the searchbox component
+-->
+<head>
+ <meta charset="utf-8">
+ <title>SearchBox component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+window.onload = function () {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SearchBox = React.createFactory(
+ browserRequire("devtools/client/shared/components/SearchBox")
+ );
+ ok(SearchBox, "Got the SearchBox factory");
+
+ async function testSimpleSearchBox() {
+ // Test initial state
+ const { component, $ } = await createComponentTest(SearchBox, {
+ type: "search",
+ keyShortcut: "CmdOrCtrl+F",
+ placeholder: "crazy placeholder",
+ });
+
+ is(component.state.value, "", "Initial value is blank");
+ ok(!component.state.focused, "Input isn't initially focused");
+ ok($(".devtools-searchinput-clear").hidden, "Clear button hidden");
+ is($(".devtools-searchinput").placeholder, "crazy placeholder",
+ "Placeholder is properly set");
+
+ synthesizeKey("f", { accelKey: true });
+ await forceRender(component); // Wait for state update
+ ok(component.state.focused, "Shortcut key focused the input box");
+
+ $(".devtools-searchinput").blur();
+ await forceRender(component);
+ ok(!component.state.focused, "`focused` state set to false after blur");
+
+ // Test changing value in state
+ await setState(component, {
+ value: "foo",
+ });
+
+ is(component.state.value, "foo", "value was properly set on state");
+ is($(".devtools-searchinput").value, "foo", "value was properly set on element");
+
+ // Filling input should show clear button
+ ok(!$(".devtools-searchinput-clear").hidden, "Clear button shown");
+
+ // Clearing value should hide clear button
+ await setState(component, {
+ value: "",
+ });
+ await forceRender(component);
+ ok($(".devtools-searchinput-clear").hidden, "Clear button was hidden");
+ }
+
+ add_task(async function () {
+ await testSimpleSearchBox();
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_sidebar_toggle.html b/devtools/client/shared/components/test/chrome/test_sidebar_toggle.html
new file mode 100644
index 0000000000..0a31037a84
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_sidebar_toggle.html
@@ -0,0 +1,59 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test sidebar toggle button
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Sidebar toggle button test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ const SidebarToggle = browserRequire("devtools/client/shared/components/SidebarToggle.js");
+
+ try {
+ await test();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function test() {
+ const output1 = shallowRenderComponent(SidebarToggle, {
+ collapsed: false,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output1.type, "button", "Output is a button element");
+ is(output1.props.title, "Expand", "Proper title is set");
+ is(output1.props.className.indexOf("pane-collapsed"), -1,
+ "Proper class name is set");
+
+ const output2 = shallowRenderComponent(SidebarToggle, {
+ collapsed: true,
+ collapsePaneTitle: "Expand",
+ expandPaneTitle: "Collapse"
+ });
+
+ is(output2.props.title, "Collapse", "Proper title is set");
+ ok(output2.props.className.includes("pane-collapsed"),
+ "Proper class name is set");
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html b/devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html
new file mode 100644
index 0000000000..174d0f87b4
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_smart-trace-grouping.html
@@ -0,0 +1,141 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SmartTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/SmartTrace"));
+ ok(SmartTrace, "Got the SmartTrace factory");
+
+ add_task(async function() {
+ const REACT_FRAMES_COUNT = 10;
+
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10,
+ functionName: null,
+ },
+ // Simulated Redux frame
+ {
+ functionName: "rootReducer",
+ filename: "https://myfile.com/loader.js -> https://myfile.com/redux.js",
+ lineNumber: 2,
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://myfile.com/loader.js -> https://myfile.com/loadee.js",
+ lineNumber: 10,
+ },
+ // Simulated react frames
+ ...(Array.from({length: REACT_FRAMES_COUNT}, (_, i) => ({
+ functionName: "internalReact" + (REACT_FRAMES_COUNT - i),
+ filename: "https://myfile.com/loader.js -> https://myfile.com/react.js",
+ lineNumber: Number(i.toString().repeat(2)),
+ }))),
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 10,
+ columnNumber: 3,
+ functionName: "onClick",
+ },
+ ];
+
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props), window.document.body);
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered SmartTrace has an element");
+
+ isDeeply(getStacktraceText(traceEl), [
+ `<anonymous> https://myfile.com/mahscripts.js:55`,
+ `rootReducer Redux`,
+ `loadFunc https://myfile.com/loadee.js:10`,
+ `▶︎ React 10`,
+ `onClick https://myfile.com/mahscripts.js:10`,
+ ], "React frames are grouped - Redux frame is not");
+
+ info("Expand React group");
+ let onReactGroupExpanded = waitFor(() =>
+ traceEl.querySelector(".frames-group.expanded"));
+ traceEl.querySelector(".group").click();
+ await onReactGroupExpanded;
+
+ isDeeply(getStacktraceText(traceEl), [
+ `<anonymous> https://myfile.com/mahscripts.js:55`,
+ `rootReducer Redux`,
+ `loadFunc https://myfile.com/loadee.js:10`,
+ `â–¼ React 10`,
+ `| internalReact10`,
+ `| internalReact9`,
+ `| internalReact8`,
+ `| internalReact7`,
+ `| internalReact6`,
+ `| internalReact5`,
+ `| internalReact4`,
+ `| internalReact3`,
+ `| internalReact2`,
+ `| internalReact1`,
+ `onClick https://myfile.com/mahscripts.js:10`,
+ ], "React frames can be expanded");
+
+ info("Collapse React group");
+ onReactGroupExpanded = waitFor(() =>
+ !traceEl.querySelector(".frames-group.expanded"));
+ traceEl.querySelector(".group").click();
+ await onReactGroupExpanded;
+
+ isDeeply(getStacktraceText(traceEl), [
+ `<anonymous> https://myfile.com/mahscripts.js:55`,
+ `rootReducer Redux`,
+ `loadFunc https://myfile.com/loadee.js:10`,
+ `▶︎ React 10`,
+ `onClick https://myfile.com/mahscripts.js:10`,
+ ], "React frames can be collapsed");
+ });
+
+ function getStacktraceText(traceElement) {
+ return Array.from(traceElement.querySelectorAll(".frame, .frames-group")).map(el => {
+ // If it's a group, we want to append an arrow representing the group state
+ if (el.classList.contains("frames-group")) {
+ const arrow = el.classList.contains("expanded") ? "▼" : "▶︎";
+ const content = el.querySelector(".group").textContent.trim();
+ return `${arrow} ${content}`;
+ }
+
+ const title = el.querySelector(".title");
+ if (el.closest(".frames-group")) {
+ return `| ${title.textContent}`;
+ }
+
+ const location = el.querySelector(".location");
+ return `${title.textContent} ${location.textContent}`;
+ });
+ }
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html b/devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html
new file mode 100644
index 0000000000..1beade0c0c
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_smart-trace-source-maps.html
@@ -0,0 +1,290 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SmartTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/SmartTrace"));
+ ok(SmartTrace, "Got the SmartTrace factory");
+
+ add_task(async function testHappyPath() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/bundle.js",
+ lineNumber: 1,
+ columnNumber: 10,
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://myfile.com/bundle.js",
+ lineNumber: 2,
+ },
+ ];
+
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ initialRenderDelay: 2000,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ const newLine = Number(line.toString().repeat(2));
+ // Resolve immediately.
+ callback({
+ url: "https://bugzilla.mozilla.org/original.js",
+ line: newLine,
+ column,
+ });
+ return () => {};
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props),
+ window.document.body.querySelector("#s1"));
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered SmartTrace has an element");
+
+ const frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 2, "SmartTrace has 2 frames");
+
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ location: "original.js:11",
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:11",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "loadFunc",
+ location: "original.js:22",
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:22",
+ });
+
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testSlowSourcemapService() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "last",
+ lineNumber: 1,
+ columnNumber: 10,
+ },
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "first",
+ lineNumber: 2,
+ columnNumber: 10,
+ },
+ ];
+
+ const sourcemapTimeout = 2000;
+ const initialRenderDelay = 300;
+ let onReadyCount = 0;
+
+ const props = {
+ stacktrace,
+ initialRenderDelay,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ // Resolve after a while.
+ setTimeout(() => {
+ const newLine = Number(line.toString().repeat(2));
+ callback({
+ url: "https://myfile.com/react.js",
+ line: newLine,
+ column,
+ });
+ }, sourcemapTimeout)
+
+ return () => {};
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props),
+ window.document.body.querySelector("#s2"));
+
+ let traceEl = ReactDOM.findDOMNode(trace);
+ ok(!traceEl, "Nothing was rendered at first");
+ is(onReadyCount, 0, "onReady isn't called if SmartTrace isn't rendered");
+
+ info("Wait for the initial delay to be over");
+ await new Promise(res => setTimeout(res, initialRenderDelay));
+
+ traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "The trace was rendered");
+
+ let frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 2, "SmartTrace has 2 frames");
+
+ info("Check that the original frames are displayed after the initial delay");
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "last",
+ location: "https://myfile.com/bundle.js:1",
+ tooltip: "View source in Debugger → https://myfile.com/bundle.js:1",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "first",
+ location: "https://myfile.com/bundle.js:2",
+ tooltip: "View source in Debugger → https://myfile.com/bundle.js:2",
+ });
+
+ is(onReadyCount, 1, "onReady was called once");
+
+ info("Check the the sourcemapped version is rendered after the sourcemapTimeout");
+ await waitFor(() => !!traceEl.querySelector(".group"));
+
+ frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ is(frameEls.length, 0, "SmartTrace has no frame");
+
+ const groups = Array.from(traceEl.querySelectorAll(".group"));
+ is(groups.length, 1, "SmartTrace has a group");
+ is(groups[0].textContent.trim(), "React 2", "A collapsed React group is displayed");
+
+ is(onReadyCount, 1, "onReady was only called once");
+ });
+
+ add_task(async function testFlakySourcemapService() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "last",
+ lineNumber: 1,
+ columnNumber: 10,
+ },
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "pending",
+ lineNumber: 2,
+ columnNumber: 10,
+ },
+ {
+ filename: "https://myfile.com/bundle.js",
+ functionName: "first",
+ lineNumber: 3,
+ columnNumber: 10,
+ },
+ ];
+
+ const initialRenderDelay = 300;
+ const onSourceMapResultDebounceDelay = 50;
+ let onReadyCount = 0;
+
+ const props = {
+ stacktrace,
+ initialRenderDelay,
+ onSourceMapResultDebounceDelay,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ // Don't call the callback for the second frame to simulate a flaky sourcemap
+ // service request.
+ if (line === 2) {
+ return () => {};
+ }
+
+ const newLine = Number(line.toString().repeat(2));
+ callback({
+ url: `https://myfile.com/file-${line}.js`,
+ line: newLine,
+ column,
+ });
+ return () => {};
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(SmartTrace(props),
+ window.document.body.querySelector("#s3"));
+
+ let traceEl = ReactDOM.findDOMNode(trace);
+ ok(!traceEl, "Nothing was rendered at first");
+ is(onReadyCount, 0, "onReady isn't called if SmartTrace isn't rendered");
+
+ info("Wait for the initial delay + debounce to be over");
+ await waitFor(() => {
+ const el = ReactDOM.findDOMNode(trace)
+ return el && el.textContent.includes("file-1.js");
+ });
+
+ traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "The trace was rendered");
+
+ const frameEls = Array.from(traceEl.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 3, "SmartTrace has 3 frames");
+
+ info("Check that the original frames are displayed even if there's no sourcemap " +
+ "response for some frames");
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "last",
+ location: "file-1.js:11",
+ tooltip: "View source in Debugger → https://myfile.com/file-1.js:11",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "pending",
+ location: "bundle.js:2",
+ tooltip: "View source in Debugger → https://myfile.com/bundle.js:2",
+ });
+
+ checkSmartFrameString({
+ el: frameEls[2],
+ functionName: "first",
+ location: "file-3.js:33",
+ tooltip: "View source in Debugger → https://myfile.com/file-3.js:33",
+ });
+
+ is(onReadyCount, 1, "onReady was only called once");
+ });
+
+};
+</script>
+<section id=s1></section>
+<section id=s2></section>
+<section id=s3></section>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_smart-trace.html b/devtools/client/shared/components/test/chrome/test_smart-trace.html
new file mode 100644
index 0000000000..eedb72cc13
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_smart-trace.html
@@ -0,0 +1,172 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+ <section id=s1></section>
+ <section id=s2></section>
+ <section id=s3></section>
+ <section id=s4></section>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const SmartTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/SmartTrace"));
+ ok(SmartTrace, "Got the SmartTrace factory");
+
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10,
+ functionName: null,
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://myfile.com/loader.js -> https://myfile.com/loadee.js",
+ lineNumber: 10,
+ },
+ ];
+
+ add_task(async function testBasic() {
+ info("Check basic rendering");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ };
+ await renderSmartTraceAndAssertContent(
+ window.document.body.querySelector("#s1"),
+ props
+ );
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testZeroDelay() {
+ info("Check rendering with source map service and 0 initial delay");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ initialRenderDelay: 0,
+ sourceMapURLService: {
+ subscribeByLocation: () => {}
+ },
+ };
+ await renderSmartTraceAndAssertContent(
+ window.document.body.querySelector("#s2"),
+ props
+ );
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testNullDelay() {
+ info("Check rendering with source map service and null initial delay");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ initialRenderDelay: 0,
+ sourceMapURLService: {
+ subscribeByLocation: () => {}
+ },
+ };
+ await renderSmartTraceAndAssertContent(
+ window.document.body.querySelector("#s3"),
+ props
+ );
+ is(onReadyCount, 1, "onReady was called once");
+ });
+
+ add_task(async function testDelay() {
+ info("Check rendering with source map service and initial delay");
+ let onReadyCount = 0;
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ onReady: () => {
+ onReadyCount++;
+ },
+ initialRenderDelay: 500,
+ sourceMapURLService: {
+ subscribeByLocation: () => {}
+ },
+ };
+ const el = window.document.body.querySelector("#s4");
+ await renderSmartTraceAndAssertContent(
+ el,
+ props,
+ false
+ );
+ is(onReadyCount, 0, "onReady wasn't called at first");
+ info(`Wait for ${props.initialRenderDelay}ms so the stacktrace should be rendered`)
+ await new Promise(res => setTimeout(res, props.initialRenderDelay))
+ is(onReadyCount, 1, "onReady was called after waiting for the initial delay");
+ assertRenderedElementContent(el);
+ });
+
+ async function renderSmartTraceAndAssertContent(el, props, shouldBeRendered = true) {
+ let trace;
+ await new Promise(resolve => {
+ trace = ReactDOM.render(SmartTrace(props), el, resolve);
+ });
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+
+ if (!shouldBeRendered) {
+ ok(!traceEl, "SmartTrace wasn't rendered initially");
+ return;
+ }
+
+ ok(traceEl, "Rendered SmartTrace has an element");
+ assertRenderedElementContent(traceEl);
+ }
+
+ function assertRenderedElementContent(el) {
+ const frameEls = Array.from(el.querySelectorAll(".frame"));
+ ok(frameEls, "Rendered SmartTrace has frames");
+ is(frameEls.length, 2, "SmartTrace has 2 frames");
+
+ checkSmartFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ location: "https://myfile.com/mahscripts.js:55",
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55",
+ });
+
+ // Check the third frame, the source should be parsed into a valid source URL
+ checkSmartFrameString({
+ el: frameEls[1],
+ functionName: "loadFunc",
+ location: "https://myfile.com/loadee.js:10",
+ tooltip: "View source in Debugger → https://myfile.com/loadee.js:10",
+ });
+ }
+
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html b/devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html
new file mode 100644
index 0000000000..7535d4d2df
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_stack-trace-source-maps.html
@@ -0,0 +1,98 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace with source maps
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function () {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/StackTrace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(async function () {
+ const stacktrace = [
+ {
+ filename: "https://bugzilla.mozilla.org/bundle.js",
+ lineNumber: 99,
+ columnNumber: 10
+ },
+ {
+ functionName: "loadFunc",
+ filename: "https://bugzilla.mozilla.org/bundle.js",
+ lineNumber: 108,
+ }
+ ];
+
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ // A mock source map service.
+ sourceMapURLService: {
+ subscribeByLocation ({ line, column }, callback) {
+ const newLine = line === 99 ? 1 : 7;
+ // Resolve immediately.
+ callback({
+ url: "https://bugzilla.mozilla.org/original.js",
+ line: newLine,
+ column,
+ });
+
+ return () => {}
+ },
+ },
+ };
+
+ const trace = ReactDOM.render(StackTrace(props), window.document.body);
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ const frameEls = Array.from(traceEl.childNodes)
+ .filter(n => n.className && n.className.includes("frame"));
+ ok(frameEls, "Rendered StackTrace has frames");
+ is(frameEls.length, 2, "StackTrace has 2 frames");
+
+ checkFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ source: "https://bugzilla.mozilla.org/original.js",
+ file: "original.js",
+ line: 1,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:1:10",
+ });
+
+ checkFrameString({
+ el: frameEls[1],
+ functionName: "loadFunc",
+ source: "https://bugzilla.mozilla.org/original.js",
+ file: "original.js",
+ line: 7,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:7",
+ });
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_stack-trace.html b/devtools/client/shared/components/test/chrome/test_stack-trace.html
new file mode 100644
index 0000000000..56d9288f06
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_stack-trace.html
@@ -0,0 +1,100 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace
+-->
+<head>
+ <meta charset="utf-8">
+ <title>StackTrace component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+"use strict";
+
+window.onload = function() {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/StackTrace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(async function() {
+ const stacktrace = [
+ {
+ filename: "https://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10,
+ },
+ {
+ asyncCause: "because",
+ functionName: "loadFunc",
+ filename: "https://myfile.com/loadee.js",
+ lineNumber: 10,
+ },
+ ];
+
+ const props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {},
+ };
+
+ const trace = ReactDOM.render(StackTrace(props), window.document.body);
+ await forceRender(trace);
+
+ const traceEl = ReactDOM.findDOMNode(trace);
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ const frameEls = Array.from(traceEl.childNodes)
+ .filter(n => n.className && n.className.includes("frame"));
+ ok(frameEls, "Rendered StackTrace has frames");
+ is(frameEls.length, 3, "StackTrace has 3 frames");
+
+ // Check the top frame, function name should be anonymous
+ checkFrameString({
+ el: frameEls[0],
+ functionName: "<anonymous>",
+ source: "https://myfile.com/mahscripts.js",
+ file: "https://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check the async cause node
+ is(frameEls[1].className, "frame-link-async-cause",
+ "Async cause has the right class");
+ is(frameEls[1].textContent, "(Async: because)", "Async cause has the right label");
+
+ // Check the third frame, the source should be parsed into a valid source URL
+ checkFrameString({
+ el: frameEls[2],
+ functionName: "loadFunc",
+ source: "https://myfile.com/loadee.js",
+ file: "https://myfile.com/loadee.js",
+ line: 10,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → https://myfile.com/loadee.js:10",
+ });
+
+ // Check the tabs and newlines in the stack trace textContent
+ const traceText = traceEl.textContent;
+ const traceLines = traceText.split("\n");
+ ok(!!traceLines.length, "There are newlines in the stack trace text");
+ is(traceLines.pop(), "", "There is a newline at the end of the stack trace text");
+ is(traceLines.length, 3, "The stack trace text has 3 lines");
+ ok(traceLines.every(l => l[0] == "\t"), "Every stack trace line starts with tab");
+ });
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tabs_accessibility.html b/devtools/client/shared/components/test/chrome/test_tabs_accessibility.html
new file mode 100644
index 0000000000..4d0ea6ef96
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tabs_accessibility.html
@@ -0,0 +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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test tabs accessibility.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component accessibility test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const InspectorTabPanel = createFactory(browserRequire("devtools/client/inspector/components/InspectorTabPanel"));
+ const Tabbar =
+ createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
+ const tabbar = Tabbar();
+ const tabbarReact = ReactDOM.render(tabbar, window.document.body);
+ const tabbarEl = ReactDOM.findDOMNode(tabbarReact);
+
+ // Setup for InspectorTabPanel
+ const tabpanels = document.createElement("div");
+ tabpanels.id = "tabpanels";
+ document.body.appendChild(tabpanels);
+
+ await addTabWithPanel(0);
+ await addTabWithPanel(1);
+
+ const tabAnchors = tabbarEl.querySelectorAll("li.tabs-menu-item a");
+
+ is(tabAnchors[0].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[0].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[0].getAttribute("aria-selected"), "true", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[0].getAttribute("aria-controls"), "sidebar-0-panel", "Anchor aria-controls is set correctly");
+ is(tabAnchors[1].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+ is(tabAnchors[1].getAttribute("role"), "tab", "Anchor role is set correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "false", "Anchor aria-selected is set correctly by default");
+ is(tabAnchors[1].getAttribute("aria-controls"), "sidebar-1-panel", "Anchor aria-controls is set correctly");
+
+ await setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ activeTab: 1
+ }));
+
+ is(tabAnchors[0].getAttribute("aria-selected"), "false", "Anchor aria-selected is reset correctly");
+ is(tabAnchors[1].getAttribute("aria-selected"), "true", "Anchor aria-selected is reset correctly");
+
+ function addTabWithPanel(tabId) {
+ // Setup for InspectorTabPanel
+ const panel = document.createElement("div");
+ panel.id = `sidebar-${tabId}`;
+ document.body.appendChild(panel);
+
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({
+ id: `sidebar-${tabId}`,
+ title: `tab-${tabId}`,
+ panel: InspectorTabPanel
+ }),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tabs_menu.html b/devtools/client/shared/components/test/chrome/test_tabs_menu.html
new file mode 100644
index 0000000000..cc4638e05a
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tabs_menu.html
@@ -0,0 +1,84 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html class="theme-light">
+<!--
+Test all-tabs menu.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tabs component All-tabs menu test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/skin/variables.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/skin/common.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/content/shared/components/tabs/Tabs.css">
+ <link rel="stylesheet" type="text/css" href="chrome://devtools/content/inspector/components/InspectorTabPanel.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { Component, createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const Tabbar = createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
+
+ // Create container for the TabBar. Set smaller width
+ // to ensure that tabs won't fit and the all-tabs menu
+ // needs to appear.
+ const tabBarBox = document.createElement("div");
+ tabBarBox.style.width = "200px";
+ tabBarBox.style.height = "200px";
+ tabBarBox.style.border = "1px solid lightgray";
+ document.body.appendChild(tabBarBox);
+
+ // Render the tab-bar.
+ const tabbar = Tabbar({
+ showAllTabsMenu: true,
+ });
+
+ const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
+
+ class TabPanelClass extends Component {
+ render() {
+ return dom.div({}, "content");
+ }
+ }
+
+ // Test panel.
+ const TabPanel = createFactory(TabPanelClass);
+
+ // Create a few panels.
+ await addTabWithPanel(1);
+ await addTabWithPanel(2);
+ await addTabWithPanel(3);
+ await addTabWithPanel(4);
+ await addTabWithPanel(5);
+
+ // Make sure the all-tabs menu is there.
+ const allTabsMenu = tabBarBox.querySelector(".all-tabs-menu");
+ ok(allTabsMenu, "All-tabs menu must be rendered");
+
+ function addTabWithPanel(tabId) {
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({id: `${tabId}`,
+ title: `tab-${tabId}`, panel: TabPanel}),
+ }));
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree-view_01.html b/devtools/client/shared/components/test/chrome/test_tree-view_01.html
new file mode 100644
index 0000000000..0acae4c1dc
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree-view_01.html
@@ -0,0 +1,290 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that TreeView component has working keyboard interactions.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>TreeView component keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = function() {
+ try {
+ const { a, button, div } =
+ require("devtools/client/shared/vendor/react-dom-factories");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ findRenderedDOMComponentWithTag,
+ scryRenderedDOMComponentsWithClass,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const TreeView =
+ browserRequire("devtools/client/shared/components/tree/TreeView");
+
+ const _props = {
+ ...TEST_TREE_VIEW_INTERFACE,
+ renderValue: props => {
+ return (props.value === "C" ?
+ div({},
+ props.value + " ",
+ a({ href: "#" }, "Focusable 1"),
+ button({ }, "Focusable 2")) :
+ props.value + ""
+ );
+ },
+ };
+ const treeView = React.createElement(TreeView, _props);
+ const tree = ReactDOM.render(treeView, document.body);
+ const treeViewEl = findRenderedDOMComponentWithClass(tree, "treeTable");
+ const rows = scryRenderedDOMComponentsWithClass(tree, "treeRow");
+ const defaultFocus = treeViewEl.ownerDocument.body;
+
+ function blurEl(el) {
+ // Simulate.blur does not seem to update the activeElement.
+ el.blur();
+ }
+
+ function focusEl(el) {
+ // Simulate.focus does not seem to update the activeElement.
+ el.focus();
+ }
+
+ const tests = [{
+ name: "Test default TreeView state. Keyboard focus is set to document " +
+ "body by default.",
+ state: { selected: null, active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Selected row must be set to the first row on initial focus. " +
+ "Keyboard focus should be set on TreeView's conatiner.",
+ action: () => {
+ focusEl(treeViewEl);
+ Simulate.click(rows[0]);
+ },
+ activeElement: treeViewEl,
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should remain set even when the treeView is " +
+ "blured. Keyboard focus should be set back to document body.",
+ action: () => blurEl(treeViewEl),
+ state: { selected: "/B" },
+ activeElement: defaultFocus,
+ }, {
+ name: "Selected row must be re-set again to the first row on initial " +
+ "focus. Keyboard focus should be set on treeView's conatiner.",
+ action: () => focusEl(treeViewEl),
+ activeElement: treeViewEl,
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should be updated to last on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/D" },
+ }, {
+ name: "Selected row should remain on last on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/D" },
+ }, {
+ name: "Selected row should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should be updated to first on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should remain on first on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should move to the next matching row with first letter navigation.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "C" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should not change when there are no more visible nodes matching first letter navigation.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "C" }},
+ state: { selected: "/C" },
+ }, {
+ name: "Selected row should be updated to last on End.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "End" }},
+ state: { selected: "/D" },
+ }, {
+ name: "Selected row should be updated to first on Home.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Home" }},
+ state: { selected: "/B" },
+ }, {
+ name: "Selected row should be set as active on Enter.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Enter" }},
+ state: { selected: "/B", active: "/B" },
+ activeElement: treeViewEl,
+ }, {
+ name: "Active row should be unset on Escape.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Escape" }},
+ state: { selected: "/B", active: null },
+ }, {
+ name: "Selected row should be set as active on Space.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: " " }},
+ state: { selected: "/B", active: "/B" },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should unset when focus leaves the treeView.",
+ action: () => blurEl(treeViewEl),
+ state: { selected: "/B", active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on treeView's conatiner on focus.",
+ action: () => focusEl(treeViewEl),
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowDown" }},
+ state: { selected: "/C", active: null },
+ }, {
+ name: "Selected row should be set as active on Enter. Keyboard focus " +
+ "should be set on the first focusable element inside the row, if " +
+ "available.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Enter" }},
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should be set to next tabbable element inside " +
+ "the active row on Tab.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the row when focused on last " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Keyboard focus should wrap inside the row when focused on first " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ return findRenderedDOMComponentWithTag(tree, "button");
+ },
+ }, {
+ name: "Active row should be unset on Escape. Focus should move back to " +
+ "the treeView container.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Escape" }},
+ state: { selected: "/C", active: null },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be set as active on Space. Keyboard focus " +
+ "should be set on the first focusable element inside the row, if " +
+ "available.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: " " }},
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Selected row should remain set even when the treeView is " +
+ "blured. Keyboard focus should be set back to document body.",
+ action: () => treeViewEl.ownerDocument.activeElement.blur(),
+ state: { selected: "/C", active: null },
+ activeElement: defaultFocus,
+ }, {
+ name: "Keyboard focus should be set on treeView's conatiner on focus.",
+ action: () => focusEl(treeViewEl),
+ state: { selected: "/C", active: null },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be set as active on Space. Keyboard focus " +
+ "should be set on the first focusable element inside the row, if " +
+ "available.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: " " }},
+ state: { selected: "/C", active: "/C" },
+ get activeElement() {
+ // When row becomes active/inactive, it is replaced with a newly
+ // rendered one.
+ return findRenderedDOMComponentWithTag(tree, "a");
+ },
+ }, {
+ name: "Selected row should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "ArrowUp" }},
+ state: { selected: "/B", active: null },
+ activeElement: treeViewEl,
+ }, {
+ name: "Selected row should be set as active on Enter.",
+ event: { type: "keyDown", el: treeViewEl, options: { key: "Enter" }},
+ state: { selected: "/B", active: "/B" },
+ activeElement: treeViewEl,
+ }, {
+ name: "Keyboard focus should move to another focusable element outside " +
+ "of the treeView when there's nothing to focus on inside the row.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ state: { selected: "/B", active: null },
+ activeElement: treeViewEl.ownerDocument.documentElement,
+ }];
+
+ for (const test of tests) {
+ const { action, event, state, name } = test;
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ Simulate[type](el, options);
+ } else if (action) {
+ action();
+ }
+
+ if (test.activeElement) {
+ is(treeViewEl.ownerDocument.activeElement, test.activeElement,
+ "Focus is set correctly.");
+ }
+
+ for (const key in state) {
+ is(tree.state[key], state[key], `${key} state is correct.`);
+ }
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree-view_02.html b/devtools/client/shared/components/test/chrome/test_tree-view_02.html
new file mode 100644
index 0000000000..77c5934a66
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree-view_02.html
@@ -0,0 +1,136 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that TreeView component filtering works with keyboard.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>TreeView component filtering keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <link rel="stylesheet" href="chrome://devtools/content/shared/components/tree/TreeView.css" type="text/css">
+ <style>
+ .treeRow.hide {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = function() {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ scryRenderedDOMComponentsWithClass,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const TreeView =
+ browserRequire("devtools/client/shared/components/tree/TreeView");
+
+ function testKeyboardInteraction(tree, treeViewEl, rows) {
+ // Expected tree when filtered (C is filtered)
+ //
+ // A
+ // |-- B
+ // `-- D
+ is(window.getComputedStyle(rows[1]).getPropertyValue("display"), "none",
+ "Row C must be hidden by default.");
+
+ const tests = [{
+ name: "Selected row must be set to the first row on initial focus. " +
+ "Keyboard focus must be set on TreeView's conatiner.",
+ action: () => {
+ Simulate.click(rows[0]);
+ },
+ activeElement: treeViewEl,
+ state: { selected: "/B" },
+ }, {
+ name: "Selecting next row must skip hidden row on ArrowDown.",
+ event: {
+ type: "keyDown",
+ el: treeViewEl,
+ options: { key: "ArrowDown" },
+ },
+ state: { selected: "/D" },
+ }, {
+ name: "Selecting previous row must be skip hidden row on ArrowUp.",
+ event: {
+ type: "keyDown",
+ el: treeViewEl,
+ options: { key: "ArrowUp" },
+ },
+ state: { selected: "/B" },
+ }];
+
+ for (const test of tests) {
+ const { action, event, state, name } = test;
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ Simulate[type](el, options);
+ } else if (action) {
+ action();
+ }
+
+ for (const key in state) {
+ is(tree.state[key], state[key], `${key} state is correct.`);
+ }
+ }
+ }
+
+ info("Test hiding rows via decorator.");
+ const props = {
+ ...TEST_TREE_VIEW_INTERFACE,
+ decorator: {
+ getRowClass: ({ label }) => {
+ if (label === "C") {
+ return ["hide"];
+ }
+ return [];
+ }
+ }
+ };
+ let treeView = React.createElement(TreeView, props);
+ let tree = ReactDOM.render(treeView, document.body);
+ let treeViewEl = findRenderedDOMComponentWithClass(tree, "treeTable");
+ let rows = scryRenderedDOMComponentsWithClass(tree, "treeRow");
+
+ testKeyboardInteraction(tree, treeViewEl, rows);
+
+ // Remove TreeView component.
+ ReactDOM.unmountComponentAtNode(document.body);
+
+ info("Test hiding rows via onFilter.");
+ props.decorator = null;
+ props.onFilter = ({ label }) => {
+ console.log(`onFILTER ${label !== "C"}`)
+ return label !== "C";
+ };
+ treeView = React.createElement(TreeView, props);
+ tree = ReactDOM.render(treeView, document.body);
+ treeViewEl = findRenderedDOMComponentWithClass(tree, "treeTable");
+ rows = scryRenderedDOMComponentsWithClass(tree, "treeRow");
+
+ testKeyboardInteraction(tree, treeViewEl, rows);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_01.html b/devtools/client/shared/components/test/chrome/test_tree_01.html
new file mode 100644
index 0000000000..0740c8957e
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_01.html
@@ -0,0 +1,68 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test trees get displayed with the items in correct order and at the correct
+depth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ ok(React, "Should get React");
+ ok(Tree, "Should get Tree");
+
+ const t = Tree(TEST_TREE_INTERFACE);
+ ok(t, "Should be able to create Tree instances");
+
+ const tree = ReactDOM.render(t, window.document.body);
+ ok(tree, "Should be able to mount Tree instances");
+ isAccessibleTree(tree);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Should get the items rendered and indented as expected");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_02.html b/devtools/client/shared/components/test/chrome/test_tree_02.html
new file mode 100644
index 0000000000..f538965572
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_02.html
@@ -0,0 +1,49 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that collapsed subtrees aren't rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ isAccessibleTree(tree);
+ TEST_TREE.expanded = new Set("MNO".split(""));
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Collapsed subtrees shouldn't be rendered");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_03.html b/devtools/client/shared/components/test/chrome/test_tree_03.html
new file mode 100644
index 0000000000..6ebefa1fb7
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_03.html
@@ -0,0 +1,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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Tree's autoExpandDepth.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 1
+ })), window.document.body);
+
+ isAccessibleTree(tree);
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Tree should be auto expanded one level");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_04.html b/devtools/client/shared/components/test/chrome/test_tree_04.html
new file mode 100644
index 0000000000..2213f72497
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_04.html
@@ -0,0 +1,133 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that we only render visible tree items.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ function getSpacerHeights() {
+ return {
+ top: document.querySelector(".tree > div:first-of-type").clientHeight,
+ bottom: document.querySelector(".tree > div:last-of-type").clientHeight,
+ };
+ }
+
+ const ITEM_HEIGHT = 3;
+
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const tree = ReactDOM.render(
+ Tree(Object.assign({}, TEST_TREE_INTERFACE, { itemHeight: ITEM_HEIGHT })),
+ window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ await setState(tree, {
+ height: 3 * ITEM_HEIGHT,
+ scroll: 1 * ITEM_HEIGHT
+ });
+
+ isAccessibleTree(tree);
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ ], "Tree should show the 2nd, 3rd, and 4th items + buffer of 1 item at each end");
+
+ let spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has the correct height");
+ is(spacers.bottom, 10 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ await setState(tree, {
+ height: 2 * ITEM_HEIGHT,
+ scroll: 3 * ITEM_HEIGHT
+ });
+
+ isAccessibleTree(tree);
+ isRenderedTree(document.body.textContent, [
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ ], "Tree should show the 4th and 5th item + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 2 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 9 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ // Set height to 2 items + 1 pixel at each end, scroll so that 4 items are visible
+ // (2 fully, 2 partially with 1 visible pixel)
+ await setState(tree, {
+ height: 2 * ITEM_HEIGHT + 2,
+ scroll: 3 * ITEM_HEIGHT - 1
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ ], "Tree should show the 4 visible items + buffer of 1 item at each end");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 1 * ITEM_HEIGHT, "Top spacer has the correct height");
+ is(spacers.bottom, 8 * ITEM_HEIGHT, "Bottom spacer has the correct height");
+
+ await setState(tree, {
+ height: 20 * ITEM_HEIGHT,
+ scroll: 0
+ });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Tree should show all rows");
+
+ spacers = getSpacerHeights();
+ is(spacers.top, 0, "Top spacer has zero height");
+ is(spacers.bottom, 0, "Bottom spacer has zero height");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_05.html b/devtools/client/shared/components/test/chrome/test_tree_05.html
new file mode 100644
index 0000000000..5427a1bd8d
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_05.html
@@ -0,0 +1,195 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test focusing with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { onFocus: x => renderTree({ focused: x }) },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+ const treeElem = document.querySelector(".tree");
+
+ isAccessibleTree(tree);
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ renderTree({ focused: "G" });
+ isAccessibleTree(tree, { hasActiveDescendant: true });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:true",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "G should be focused");
+
+ // When tree gets focus by means other than mouse, do not set first node as
+ // focused node when there is already a focused node.
+ Simulate.focus(treeElem);
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:true",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "G should remain focused");
+
+ // Click the first tree node
+ document.querySelector(".tree-node").click();
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "A should be focused");
+
+ // Mouse down and mouse up events set tree "mouseDown" state correctly.
+ ok(!tree.state.mouseDown, "Mouse down state is not set.");
+ Simulate.mouseDown(document.querySelector(".tree-node"));
+ ok(tree.state.mouseDown, "Mouse down state is set.");
+ Simulate.mouseUp(document.querySelector(".tree-node"));
+ ok(!tree.state.mouseDown, "Mouse down state is reset.");
+
+ // Unset focused tree state.
+ renderTree({ focused: null });
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "No node should be focused");
+
+ // When tree gets focus while mouse is down, do not set first node as
+ // focused node.
+ Simulate.mouseDown(document.querySelector(".tree-node"));
+ Simulate.focus(treeElem);
+ Simulate.mouseUp(document.querySelector(".tree-node"));
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "No node should have been focused");
+
+ // When tree gets focus by means other than mouse, set first node as focused
+ // node if no nodes are focused.
+ Simulate.focus(treeElem);
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "A should be focused");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_06.html b/devtools/client/shared/components/test/chrome/test_tree_06.html
new file mode 100644
index 0000000000..c8d1aa5e9f
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_06.html
@@ -0,0 +1,340 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test keyboard navigation with the Tree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { onFocus: x => renderTree({ focused: x }) },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+
+ isAccessibleTree(tree);
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // UP ----------------------------------------------------------------------
+
+ info("Up to the previous sibling.");
+ renderTree({ focused: "L" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, K should be focused.");
+
+ info("Up to the parent.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, E should be focused.");
+
+ info("Try and navigate up, past the first item.");
+ renderTree({ focused: "A" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the UP, A should be focused and we shouldn't have overflowed past it.");
+
+ // DOWN --------------------------------------------------------------------
+
+ info("Down to next sibling.");
+ renderTree({ focused: "K" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, L should be focused.");
+
+ info("Down to parent's next sibling.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:true",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the DOWN, F should be focused.");
+
+ info("Try and go down past the last item.");
+ renderTree({ focused: "O" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the DOWN, O should still be focused " +
+ "and we shouldn't have overflowed past it.");
+
+ // LEFT --------------------------------------------------------------------
+
+ info("Left to go to parent.");
+ renderTree({ focused: "L" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E should be focused.");
+
+ info("Left to collapse children.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the LEFT, E's children should be collapsed.");
+
+ // RIGHT -------------------------------------------------------------------
+
+ info("Right to expand children.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, E's children should be expanded again.");
+
+ info("Right on already expanded node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:true",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT on already expanded node, E should remain focused.");
+
+ info("Right when preventNavigationOnArrowRight is unset to go to next item.");
+ renderTree({ focused: "E", preventNavigationOnArrowRight: false });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the RIGHT, K should be focused.");
+
+ // Check that keys are ignored if any modifier is present.
+ const keysWithModifier = [
+ { key: "ArrowDown", altKey: true },
+ { key: "ArrowDown", ctrlKey: true },
+ { key: "ArrowDown", metaKey: true },
+ { key: "ArrowDown", shiftKey: true },
+ ];
+ await forceRender(tree);
+
+ for (const key of keysWithModifier) {
+ Simulate.keyDown(document.querySelector(".tree"), key);
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After DOWN + (alt|ctrl|meta|shift), K should remain focused.");
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_07.html b/devtools/client/shared/components/test/chrome/test_tree_07.html
new file mode 100644
index 0000000000..2e763aaf20
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_07.html
@@ -0,0 +1,69 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that arrows get the open attribute when their item's children are expanded.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const Tree =
+ React.createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ const treeProps = Object.assign({}, TEST_TREE_INTERFACE, {
+ renderItem: (item, depth, focused, arrow) => {
+ return dom.div(
+ {
+ id: item,
+ style: { marginLeft: depth * 16 + "px" }
+ },
+ arrow,
+ item
+ );
+ }
+ });
+ const tree = ReactDOM.render(Tree(treeProps), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ await forceRender(tree);
+
+ let arrows = document.querySelectorAll(".arrow");
+ for (const a of arrows) {
+ ok(a.classList.contains("open"), "Every arrow should be open.");
+ }
+
+ TEST_TREE.expanded = new Set();
+ await forceRender(tree);
+
+ arrows = document.querySelectorAll(".arrow");
+ for (const a of arrows) {
+ ok(!a.classList.contains("open"), "Every arrow should be closed.");
+ }
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_08.html b/devtools/client/shared/components/test/chrome/test_tree_08.html
new file mode 100644
index 0000000000..cfdff8090d
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_08.html
@@ -0,0 +1,61 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is clicked, it steals focus from
+other inputs.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { onFocus: x => renderTree({ focused: x }) },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+
+ input.focus();
+ is(document.activeElement, input, "The text input should be focused.");
+
+ document.querySelector(".tree-node").click();
+ await forceRender(tree);
+
+ isnot(document.activeElement, input,
+ "The input should have had it's focus stolen by clicking on a tree item.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_09.html b/devtools/client/shared/components/test/chrome/test_tree_09.html
new file mode 100644
index 0000000000..4d6a1010b5
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_09.html
@@ -0,0 +1,85 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ let numberOfExpands = 0;
+ let lastExpandedItem = null;
+
+ let numberOfCollapses = 0;
+ let lastCollapsedItem = null;
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ autoExpandDepth: 0,
+ onExpand: item => {
+ lastExpandedItem = item;
+ numberOfExpands++;
+ TEST_TREE.expanded.add(item);
+ },
+ onCollapse: item => {
+ lastCollapsedItem = item;
+ numberOfCollapses++;
+ TEST_TREE.expanded.delete(item);
+ },
+ onFocus: item => renderTree({ focused: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree({ focused: "A" });
+
+ is(lastExpandedItem, null);
+ is(lastCollapsedItem, null);
+
+ // Expand "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ await forceRender(tree);
+
+ is(lastExpandedItem, "A", "Our onExpand callback should have been fired.");
+ is(numberOfExpands, 1);
+
+ // Now collapse "A" via the keyboard and then let the component re-render.
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ await forceRender(tree);
+
+ is(lastCollapsedItem, "A", "Our onCollapsed callback should have been fired.");
+ is(numberOfCollapses, 1);
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_10.html b/devtools/client/shared/components/test/chrome/test_tree_10.html
new file mode 100644
index 0000000000..7cda9e4348
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_10.html
@@ -0,0 +1,57 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is expanded or collapsed the appropriate event handler fires.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ { autoExpandDepth: 1 },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ renderTree({ focused: "A" });
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "-C:false",
+ "-D:false",
+ "M:false",
+ "-N:false",
+ ], "Should have auto-expanded one level.");
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_11.html b/devtools/client/shared/components/test/chrome/test_tree_11.html
new file mode 100644
index 0000000000..612a851018
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_11.html
@@ -0,0 +1,100 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that when an item in the Tree component is focused by arrow key, the view is scrolled.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ .tree {
+ height: 30px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+'use strict'
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ itemHeight: 10,
+ onFocus: item => renderTree({ focused: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree({ focused: "K" });
+
+ tree.setState({ scroll: 10 });
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ ], "Should render initial correctly");
+
+ await new Promise(resolve => {
+ const treeElem = document.querySelector(".tree");
+ treeElem.addEventListener("scroll", function onScroll() {
+ dumpn("Got scroll event");
+ treeElem.removeEventListener("scroll", onScroll);
+ resolve();
+ });
+
+ dumpn("Sending ArrowDown key");
+ Simulate.keyDown(treeElem, { key: "ArrowDown" });
+ });
+
+ dumpn("Forcing re-render");
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:true",
+ "--F:false",
+ ], "Should have scrolled down one");
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_12.html b/devtools/client/shared/components/test/chrome/test_tree_12.html
new file mode 100644
index 0000000000..4bcf7ef705
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_12.html
@@ -0,0 +1,146 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test keyboard navigation/activation with the VirtualizedTree component.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ function renderTree(props) {
+ const treeProps = {
+ ...TEST_TREE_INTERFACE,
+ onFocus: x => renderTree({ focused: x }),
+ ...props
+ };
+
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ const tree = renderTree();
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // Test Home key -----------------------------------------------------------
+
+ info("Press Home to move to the first node.");
+ renderTree({ focused: "L" });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "Home" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the Home key, A should be focused.");
+
+ info("Press Home again when already on first node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "Home" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "After the Home key again, A should still be focused.");
+
+ // Test End key ------------------------------------------------------------
+
+ info("Press End to move to the last node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "End" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the End key, O should be focused.");
+
+ info("Press End again when already on last node.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "End" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ "--I:false",
+ "-D:false",
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "After the End key again, O should still be focused.");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_13.html b/devtools/client/shared/components/test/chrome/test_tree_13.html
new file mode 100644
index 0000000000..183e144c82
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_13.html
@@ -0,0 +1,88 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test trees have the correct scroll position when they are resized.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <style>
+ .tree {
+ height: 50px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function() {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(
+ browserRequire("devtools/client/shared/components/VirtualizedTree"));
+ const ITEM_HEIGHT = 10;
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = {
+ ...TEST_TREE_INTERFACE,
+ itemHeight: ITEM_HEIGHT,
+ onFocus: item => renderTree({ focused: item }),
+ ...props
+ };
+ return ReactDOM.render(Tree(treeProps), document.body);
+ }
+
+ const tree = renderTree({ focused: "L" });
+ const treeEl = tree.refs.tree;
+
+ is(tree.state.scroll, 0, "Scroll position should be 0 by default");
+ is(treeEl.scrollTop, 0, "Tree scrollTop should be 0 by default");
+
+ info(`Focus on the next node and scroll by ${ITEM_HEIGHT}`);
+ Simulate.keyDown(treeEl, { key: "ArrowDown" });
+ await forceRender(tree);
+
+ is(tree.state.scroll, ITEM_HEIGHT, `Scroll position should now be ${ITEM_HEIGHT}`);
+ is(treeEl.scrollTop, ITEM_HEIGHT,
+ `Tree scrollTop should now be ${ITEM_HEIGHT}`);
+
+ info("Simulate window resize along with scroll back to top");
+ treeEl.scrollTo({ left: 0, top: 0 });
+ window.dispatchEvent(new Event("resize"));
+ await forceRender(tree);
+
+ is(tree.state.scroll, ITEM_HEIGHT,
+ `Scroll position should remain at ${ITEM_HEIGHT}`);
+ is(treeEl.scrollTop, ITEM_HEIGHT,
+ `Tree scrollTop should remain at ${ITEM_HEIGHT}`);
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_14.html b/devtools/client/shared/components/test/chrome/test_tree_14.html
new file mode 100644
index 0000000000..d68d87d6c5
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_14.html
@@ -0,0 +1,245 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that Tree component has working keyboard interactions.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component keyboard test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function() {
+ try {
+ const { a, button, div } =
+ require("devtools/client/shared/vendor/react-dom-factories");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const {
+ Simulate,
+ findRenderedDOMComponentWithClass,
+ findRenderedDOMComponentWithTag,
+ } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree = createFactory(
+ browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ let gTree, gFocused, gActive;
+ function renderTree(props = {}) {
+ let toggle = true;
+ const treeProps = {
+ ...TEST_TREE_INTERFACE,
+ onFocus: x => {
+ gFocused = x;
+ renderTree({ focused: gFocused, active: gActive });
+ },
+ onActivate: x => {
+ gActive = x;
+ renderTree({ focused: gFocused, active: gActive });
+ },
+ renderItem: (x, depth, focused) => {
+ toggle = !toggle;
+ return toggle ?
+ (div(
+ {},
+ `${"-".repeat(depth)}${x}:${focused}`,
+ a({ href: "#" }, "Focusable 1"),
+ button({ }, "Focusable 2"),
+ "\n",
+ )
+ ) : `${"-".repeat(depth)}${x}:${focused}`;
+ },
+ ...props
+ };
+
+ gTree = ReactDOM.render(Tree(treeProps), document.body);
+ }
+
+ renderTree();
+ const els = {
+ get tree() {
+ // React will replace the tree via renderTree.
+ return findRenderedDOMComponentWithClass(gTree, "tree");
+ },
+ get anchor() {
+ // When tree node becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(gTree, "a");
+ },
+ get button() {
+ // When tree node becomes active/inactive, it is replaced with a newly rendered
+ // one.
+ return findRenderedDOMComponentWithTag(gTree, "button");
+ },
+ };
+
+ const tests = [{
+ name: "Test default Tree props. Keyboard focus is set to document body by default.",
+ props: { focused: undefined, active: undefined },
+ activeElement: document.body,
+ }, {
+ name: "Focused props must be set to the first node on initial focus. " +
+ "Keyboard focus should be set on the tree.",
+ action: () => els.tree.focus(),
+ activeElement: "tree",
+ props: { focused: "A" },
+ }, {
+ name: "Focused node should remain set even when the tree is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => els.tree.blur(),
+ props: { focused: "A" },
+ activeElement: document.body,
+ }, {
+ name: "Unset tree's focused prop.",
+ action: () => renderTree({ focused: null }),
+ props: { focused: null },
+ }, {
+ name: "Focused node must be re-set again to the first tree node on initial " +
+ "focus. Keyboard focus should be set on tree's conatiner.",
+ action: () => els.tree.focus(),
+ activeElement: "tree",
+ props: { focused: "A" },
+ }, {
+ name: "Focused node should be set as active on Enter.",
+ event: { type: "keyDown", el: "tree", options: { key: "Enter" }},
+ props: { focused: "A", active: "A" },
+ activeElement: "tree",
+ }, {
+ name: "Active node should be unset on Escape.",
+ event: { type: "keyDown", el: "tree", options: { key: "Escape" }},
+ props: { focused: "A", active: null },
+ }, {
+ name: "Focused node should be set as active on Space.",
+ event: { type: "keyDown", el: "tree", options: { key: " " }},
+ props: { focused: "A", active: "A" },
+ activeElement: "tree",
+ }, {
+ name: "Active node should unset when focus leaves the tree.",
+ action: () => els.tree.blur(),
+ props: { focused: "A", active: null },
+ activeElement: document.body,
+ }, {
+ name: "Keyboard focus should be set on tree's conatiner on focus.",
+ action: () => els.tree.focus(),
+ activeElement: "tree",
+ }, {
+ name: "Focused node should be updated to next on ArrowDown.",
+ event: { type: "keyDown", el: "tree", options: { key: "ArrowDown" }},
+ props: { focused: "M", active: null },
+ }, {
+ name: "Focused item should be set as active on Enter. Keyboard focus should be " +
+ "set on the first focusable element inside the tree node, if available.",
+ event: { type: "keyDown", el: "tree", options: { key: "Enter" }},
+ props: { focused: "M", active: "M" },
+ activeElement: "anchor",
+ }, {
+ name: "Keyboard focus should be set to next tabbable element inside the active " +
+ "node on Tab.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ props: { focused: "M", active: "M" },
+ activeElement: "button",
+ }, {
+ name: "Keyboard focus should wrap inside the tree node when focused on last " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab");
+ },
+ props: { focused: "M", active: "M" },
+ activeElement: "anchor",
+ }, {
+ name: "Keyboard focus should wrap inside the tree node when focused on first " +
+ "tabbable element.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ props: { focused: "M", active: "M" },
+ activeElement: "button",
+ }, {
+ name: "Active tree node should be unset on Escape. Focus should move back to the " +
+ "tree container.",
+ event: { type: "keyDown", el: "tree", options: { key: "Escape" }},
+ props: { focused: "M", active: null },
+ activeElement: "tree",
+ }, {
+ name: "Focused node should be set as active on Space. Keyboard focus should be " +
+ "set on the first focusable element inside the tree node, if available.",
+ event: { type: "keyDown", el: "tree", options: { key: " " }},
+ props: { focused: "M", active: "M" },
+ activeElement: "anchor",
+ }, {
+ name: "Focused tree node should remain set even when the tree is blured. " +
+ "Keyboard focus should be set back to document body.",
+ action: () => document.activeElement.blur(),
+ props: { focused: "M", active: null, },
+ activeElement: document.body,
+ }, {
+ name: "Keyboard focus should be set on tree's conatiner on focus.",
+ action: () => els.tree.focus(),
+ props: { focused: "M", active: null },
+ activeElement: "tree",
+ }, {
+ name: "Focused tree node should be updated to previous on ArrowUp.",
+ event: { type: "keyDown", el: "tree", options: { key: "ArrowUp" }},
+ props: { focused: "A", active: null },
+ }, {
+ name: "Focused item should be set as active on Enter.",
+ event: { type: "keyDown", el: "tree", options: { key: "Enter" }},
+ props: { focused: "A", active: "A" },
+ activeElement: "tree",
+ }, {
+ name: "Keyboard focus should move to another focusable element outside of the " +
+ "tree when there's nothing to focus on inside the tree node.",
+ action() {
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ },
+ props: { focused: "A", active: null },
+ activeElement: document.documentElement,
+ }];
+
+ for (const test of tests) {
+ const { action, event, props, name } = test;
+
+ info(name);
+ if (event) {
+ const { type, options, el } = event;
+ const target = typeof el === "string" ? els[el] : el;
+ Simulate[type](target, options);
+ } else if (action) {
+ action();
+ }
+
+ await forceRender(gTree);
+
+ if (test.activeElement) {
+ const expected = typeof test.activeElement === "string" ?
+ els[test.activeElement] : test.activeElement;
+ // eslint-disable-next-line no-debugger
+ if (document.activeElement!==expected) {debugger;}
+ is(document.activeElement, expected, "Focus is set correctly.");
+ }
+
+ for (const key in props) {
+ is(gTree.props[key], props[key], `${key} prop is correct.`);
+ }
+ }
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_15.html b/devtools/client/shared/components/test/chrome/test_tree_15.html
new file mode 100644
index 0000000000..399e3d9ecd
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_15.html
@@ -0,0 +1,99 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test scroll position when focusing items in traversal but not rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ .tree {
+ height: 30px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } =
+ browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ itemHeight: 10,
+ onFocus: item => renderTree({ focused: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ info("Test first focused.");
+ const tree = renderTree({ focused: "A" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render initial correctly");
+
+ info("Test last item focused when it was not yet rendered.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "End" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:true",
+ ], "Should render last focused item correctly");
+
+ info("Test first item focused when it was not yet rendered.");
+ Simulate.keyDown(document.querySelector(".tree"), { key: "Home" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:true",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render first focused item correctly");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/chrome/test_tree_16.html b/devtools/client/shared/components/test/chrome/test_tree_16.html
new file mode 100644
index 0000000000..b70e63eade
--- /dev/null
+++ b/devtools/client/shared/components/test/chrome/test_tree_16.html
@@ -0,0 +1,145 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test scroll position when showing items both in traversal and/or rendered.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tree component test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
+ <style>
+ .tree {
+ height: 30px;
+ overflow: auto;
+ display: block;
+ }
+
+ .tree-node {
+ font-size: 10px;
+ height: 10px;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript"></script>
+<script type="application/javascript">
+
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const { createFactory } = browserRequire("devtools/client/shared/vendor/react");
+ const Tree =
+ createFactory(browserRequire("devtools/client/shared/components/VirtualizedTree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ function renderTree(props) {
+ const treeProps = Object.assign({},
+ TEST_TREE_INTERFACE,
+ {
+ itemHeight: 10,
+ onFocus: item => renderTree({ shown: item })
+ },
+ props
+ );
+ return ReactDOM.render(Tree(treeProps), window.document.body);
+ }
+
+ info("Test first shown.");
+ const tree = renderTree({ shown: "A" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render initial correctly");
+
+ info("Test last as shown when it was not yet rendered.");
+ renderTree({ shown: "O" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "--J:false",
+ "M:false",
+ "-N:false",
+ "--O:false",
+ ], "Should render shown item correctly");
+
+ info("Test first item shown when it's not first rendered.");
+ renderTree({ shown: "A" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:false",
+ ], "Should render shown item correctly");
+
+ info("Test mid item shown when it's not first rendered.");
+ renderTree({ shown: "G" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---K:false",
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ ], "Should render shown item correctly");
+
+ info("Test mid item shown when it's already rendered.");
+ renderTree({ shown: "C" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ ], "Should render shown item correctly");
+
+ info("Test item that is not in traversal.");
+ renderTree({ shown: "Z" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ ], "Should render without changes");
+
+ info("Test item that is already shown.");
+ renderTree({ shown: "F" });
+ await forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "---L:false",
+ "--F:false",
+ "--G:false",
+ "-C:false",
+ "--H:false",
+ ], "Should render without changes");
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+};
+</script>
+</pre>
+</body>
+</html>