diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/shared/components/test/chrome | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/components/test/chrome')
44 files changed, 6399 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.toml b/devtools/client/shared/components/test/chrome/chrome.toml new file mode 100644 index 0000000000..ff9a5f1883 --- /dev/null +++ b/devtools/client/shared/components/test/chrome/chrome.toml @@ -0,0 +1,87 @@ +[DEFAULT] +support-files = [ + "head.js", + "accordion.snapshots.js", +] + +["test_GridElementWidthResizer.html"] + +["test_GridElementWidthResizer_RTL.html"] + +["test_HSplitBox_01.html"] + +["test_accordion.html"] + +["test_frame_01.html"] + +["test_frame_02.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-with-autocomplete.html"] + +["test_searchbox.html"] + +["test_sidebar_toggle.html"] + +["test_smart-trace-grouping.html"] + +["test_smart-trace-source-maps.html"] + +["test_smart-trace.html"] + +["test_stack-trace-source-maps.html"] + +["test_stack-trace.html"] + +["test_tabs_accessibility.html"] + +["test_tabs_menu.html"] + +["test_tree-view_01.html"] + +["test_tree-view_02.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"] 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> |