summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components')
-rw-r--r--devtools/client/shared/components/.eslintrc.js7
-rw-r--r--devtools/client/shared/components/frame.js239
-rw-r--r--devtools/client/shared/components/h-split-box.js154
-rw-r--r--devtools/client/shared/components/moz.build27
-rw-r--r--devtools/client/shared/components/notification-box.css95
-rw-r--r--devtools/client/shared/components/notification-box.js263
-rw-r--r--devtools/client/shared/components/reps/array.js186
-rw-r--r--devtools/client/shared/components/reps/attribute.js70
-rw-r--r--devtools/client/shared/components/reps/caption.js31
-rw-r--r--devtools/client/shared/components/reps/comment-node.js60
-rw-r--r--devtools/client/shared/components/reps/date-time.js70
-rw-r--r--devtools/client/shared/components/reps/document.js78
-rw-r--r--devtools/client/shared/components/reps/element-node.js114
-rw-r--r--devtools/client/shared/components/reps/event.js81
-rw-r--r--devtools/client/shared/components/reps/function.js73
-rw-r--r--devtools/client/shared/components/reps/grip-array.js198
-rw-r--r--devtools/client/shared/components/reps/grip-map.js193
-rw-r--r--devtools/client/shared/components/reps/grip.js247
-rw-r--r--devtools/client/shared/components/reps/infinity.js41
-rw-r--r--devtools/client/shared/components/reps/long-string.js71
-rw-r--r--devtools/client/shared/components/reps/moz.build40
-rw-r--r--devtools/client/shared/components/reps/nan.js41
-rw-r--r--devtools/client/shared/components/reps/null.js46
-rw-r--r--devtools/client/shared/components/reps/number.js51
-rw-r--r--devtools/client/shared/components/reps/object-with-text.js76
-rw-r--r--devtools/client/shared/components/reps/object-with-url.js76
-rw-r--r--devtools/client/shared/components/reps/object.js171
-rw-r--r--devtools/client/shared/components/reps/promise.js111
-rw-r--r--devtools/client/shared/components/reps/prop-rep.js70
-rw-r--r--devtools/client/shared/components/reps/regexp.js63
-rw-r--r--devtools/client/shared/components/reps/rep-utils.js160
-rw-r--r--devtools/client/shared/components/reps/rep.js144
-rw-r--r--devtools/client/shared/components/reps/reps.css174
-rw-r--r--devtools/client/shared/components/reps/string.js69
-rw-r--r--devtools/client/shared/components/reps/stylesheet.js77
-rw-r--r--devtools/client/shared/components/reps/symbol.js48
-rw-r--r--devtools/client/shared/components/reps/text-node.js94
-rw-r--r--devtools/client/shared/components/reps/undefined.js46
-rw-r--r--devtools/client/shared/components/reps/window.js73
-rw-r--r--devtools/client/shared/components/search-box.js110
-rw-r--r--devtools/client/shared/components/sidebar-toggle.css32
-rw-r--r--devtools/client/shared/components/sidebar-toggle.js66
-rw-r--r--devtools/client/shared/components/splitter/draggable.js54
-rw-r--r--devtools/client/shared/components/splitter/moz.build11
-rw-r--r--devtools/client/shared/components/splitter/split-box.css88
-rw-r--r--devtools/client/shared/components/splitter/split-box.js205
-rw-r--r--devtools/client/shared/components/stack-trace.js68
-rw-r--r--devtools/client/shared/components/tabs/moz.build12
-rw-r--r--devtools/client/shared/components/tabs/tabbar.css53
-rw-r--r--devtools/client/shared/components/tabs/tabbar.js204
-rw-r--r--devtools/client/shared/components/tabs/tabs.css183
-rw-r--r--devtools/client/shared/components/tabs/tabs.js369
-rw-r--r--devtools/client/shared/components/test/browser/.eslintrc.js6
-rw-r--r--devtools/client/shared/components/test/browser/browser.ini7
-rw-r--r--devtools/client/shared/components/test/browser/browser_notification_box_basic.js36
-rw-r--r--devtools/client/shared/components/test/mochitest/.eslintrc.js6
-rw-r--r--devtools/client/shared/components/test/mochitest/chrome.ini51
-rw-r--r--devtools/client/shared/components/test/mochitest/head.js217
-rw-r--r--devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html126
-rw-r--r--devtools/client/shared/components/test/mochitest/test_frame_01.html309
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_01.html108
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_02.html70
-rw-r--r--devtools/client/shared/components/test/mochitest/test_notification_box_03.html84
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_array.html259
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_attribute.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_comment-node.html80
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_date-time.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_document.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_element-node.html341
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_event.html300
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_function.html206
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip-array.html707
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip-map.html405
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_grip.html887
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_infinity.html73
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_long-string.html125
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_nan.html48
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_null.html44
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_number.html97
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html54
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html60
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_object.html225
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_promise.html333
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_regexp.html51
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_string.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html54
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_symbol.html77
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_text-node.html115
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_undefined.html47
-rw-r--r--devtools/client/shared/components/test/mochitest/test_reps_window.html58
-rw-r--r--devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html56
-rw-r--r--devtools/client/shared/components/test/mochitest/test_stack-trace.html102
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html79
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tabs_menu.html81
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_01.html64
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_02.html45
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_03.html46
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_04.html128
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_05.html83
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_06.html320
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_07.html64
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_08.html51
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_09.html77
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_10.html52
-rw-r--r--devtools/client/shared/components/test/mochitest/test_tree_11.html92
-rw-r--r--devtools/client/shared/components/tree.js773
-rw-r--r--devtools/client/shared/components/tree/label-cell.js66
-rw-r--r--devtools/client/shared/components/tree/moz.build14
-rw-r--r--devtools/client/shared/components/tree/object-provider.js90
-rw-r--r--devtools/client/shared/components/tree/tree-cell.js101
-rw-r--r--devtools/client/shared/components/tree/tree-header.js100
-rw-r--r--devtools/client/shared/components/tree/tree-row.js184
-rw-r--r--devtools/client/shared/components/tree/tree-view.css157
-rw-r--r--devtools/client/shared/components/tree/tree-view.js352
114 files changed, 14496 insertions, 0 deletions
diff --git a/devtools/client/shared/components/.eslintrc.js b/devtools/client/shared/components/.eslintrc.js
new file mode 100644
index 000000000..3112895e9
--- /dev/null
+++ b/devtools/client/shared/components/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "define": true,
+ }
+};
diff --git a/devtools/client/shared/components/frame.js b/devtools/client/shared/components/frame.js
new file mode 100644
index 000000000..5abe5f057
--- /dev/null
+++ b/devtools/client/shared/components/frame.js
@@ -0,0 +1,239 @@
+/* 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";
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { getSourceNames, parseURL,
+ isScratchpadScheme, getSourceMappedFile } = require("devtools/client/shared/source-utils");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+const webl10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
+
+module.exports = createClass({
+ displayName: "Frame",
+
+ propTypes: {
+ // SavedFrame, or an object containing all the required properties.
+ frame: PropTypes.shape({
+ functionDisplayName: PropTypes.string,
+ source: PropTypes.string.isRequired,
+ line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ }).isRequired,
+ // Clicking on the frame link -- probably should link to the debugger.
+ onClick: PropTypes.func.isRequired,
+ // Option to display a function name before the source link.
+ showFunctionName: PropTypes.bool,
+ // Option to display a function name even if it's anonymous.
+ showAnonymousFunctionName: PropTypes.bool,
+ // Option to display a host name after the source link.
+ showHost: PropTypes.bool,
+ // Option to display a host name if the filename is empty or just '/'
+ showEmptyPathAsHost: PropTypes.bool,
+ // Option to display a full source instead of just the filename.
+ showFullSourceUrl: PropTypes.bool,
+ // Service to enable the source map feature for console.
+ sourceMapService: PropTypes.object,
+ },
+
+ getDefaultProps() {
+ return {
+ showFunctionName: false,
+ showAnonymousFunctionName: false,
+ showHost: false,
+ showEmptyPathAsHost: false,
+ showFullSourceUrl: false,
+ };
+ },
+
+ componentWillMount() {
+ const sourceMapService = this.props.sourceMapService;
+ if (sourceMapService) {
+ const source = this.getSource();
+ sourceMapService.subscribe(source, this.onSourceUpdated);
+ }
+ },
+
+ componentWillUnmount() {
+ const sourceMapService = this.props.sourceMapService;
+ if (sourceMapService) {
+ const source = this.getSource();
+ sourceMapService.unsubscribe(source, this.onSourceUpdated);
+ }
+ },
+
+ /**
+ * Component method to update the FrameView when a resolved location is available
+ * @param event
+ * @param location
+ */
+ onSourceUpdated(event, location, resolvedLocation) {
+ const frame = this.getFrame(resolvedLocation);
+ this.setState({
+ frame,
+ isSourceMapped: true,
+ });
+ },
+
+ /**
+ * Utility method to convert the Frame object to the
+ * Source Object model required by SourceMapService
+ * @param frame
+ * @returns {{url: *, line: *, column: *}}
+ */
+ getSource(frame) {
+ frame = frame || this.props.frame;
+ const { source, line, column } = frame;
+ return {
+ url: source,
+ line,
+ column,
+ };
+ },
+
+ /**
+ * Utility method to convert the Source object model to the
+ * Frame object model required by FrameView class.
+ * @param source
+ * @returns {{source: *, line: *, column: *, functionDisplayName: *}}
+ */
+ getFrame(source) {
+ const { url, line, column } = source;
+ return {
+ source: url,
+ line,
+ column,
+ functionDisplayName: this.props.frame.functionDisplayName,
+ };
+ },
+
+ render() {
+ let frame, isSourceMapped;
+ let {
+ onClick,
+ showFunctionName,
+ showAnonymousFunctionName,
+ showHost,
+ showEmptyPathAsHost,
+ showFullSourceUrl
+ } = this.props;
+
+ if (this.state && this.state.isSourceMapped) {
+ frame = this.state.frame;
+ isSourceMapped = this.state.isSourceMapped;
+ } else {
+ frame = this.props.frame;
+ }
+
+ let source = frame.source ? String(frame.source) : "";
+ let line = frame.line != void 0 ? Number(frame.line) : null;
+ let column = frame.column != void 0 ? Number(frame.column) : null;
+
+ const { short, long, host } = getSourceNames(source);
+ // Reparse the URL to determine if we should link this; `getSourceNames`
+ // has already cached this indirectly. We don't want to attempt to
+ // link to "self-hosted" and "(unknown)". However, we do want to link
+ // to Scratchpad URIs.
+ // Source mapped sources might not necessary linkable, but they
+ // are still valid in the debugger.
+ const isLinkable = !!(isScratchpadScheme(source) || parseURL(source))
+ || isSourceMapped;
+ const elements = [];
+ const sourceElements = [];
+ let sourceEl;
+
+ let tooltip = long;
+
+ // Exclude all falsy values, including `0`, as line numbers start with 1.
+ if (line) {
+ tooltip += `:${line}`;
+ // Intentionally exclude 0
+ if (column) {
+ tooltip += `:${column}`;
+ }
+ }
+
+ let attributes = {
+ "data-url": long,
+ className: "frame-link",
+ };
+
+ if (showFunctionName) {
+ let functionDisplayName = frame.functionDisplayName;
+ if (!functionDisplayName && showAnonymousFunctionName) {
+ functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction");
+ }
+
+ if (functionDisplayName) {
+ elements.push(
+ dom.span({ className: "frame-link-function-display-name" },
+ functionDisplayName),
+ " "
+ );
+ }
+ }
+
+ let displaySource = showFullSourceUrl ? long : short;
+ if (isSourceMapped) {
+ displaySource = getSourceMappedFile(displaySource);
+ } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
+ displaySource = host;
+ }
+
+ sourceElements.push(dom.span({
+ className: "frame-link-filename",
+ }, displaySource));
+
+ // If we have a line number > 0.
+ if (line) {
+ let lineInfo = `:${line}`;
+ // Add `data-line` attribute for testing
+ attributes["data-line"] = line;
+
+ // Intentionally exclude 0
+ if (column) {
+ lineInfo += `:${column}`;
+ // Add `data-column` attribute for testing
+ attributes["data-column"] = column;
+ }
+
+ sourceElements.push(dom.span({ className: "frame-link-line" }, lineInfo));
+ }
+
+ // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
+ // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
+ let sourceInnerEl = dom.span({
+ className: "frame-link-source-inner",
+ title: isLinkable ?
+ l10n.getFormatStr("frame.viewsourceindebugger", tooltip) : tooltip,
+ }, sourceElements);
+
+ // If source is not a URL (self-hosted, eval, etc.), don't make
+ // it an anchor link, as we can't link to it.
+ if (isLinkable) {
+ sourceEl = dom.a({
+ onClick: e => {
+ e.preventDefault();
+ onClick(this.getSource(frame));
+ },
+ href: source,
+ className: "frame-link-source",
+ draggable: false,
+ }, sourceInnerEl);
+ } else {
+ sourceEl = dom.span({
+ className: "frame-link-source",
+ }, sourceInnerEl);
+ }
+ elements.push(sourceEl);
+
+ if (showHost && host) {
+ elements.push(" ", dom.span({ className: "frame-link-host" }, host));
+ }
+
+ return dom.span(attributes, ...elements);
+ }
+});
diff --git a/devtools/client/shared/components/h-split-box.js b/devtools/client/shared/components/h-split-box.js
new file mode 100644
index 000000000..d804a6ba1
--- /dev/null
+++ b/devtools/client/shared/components/h-split-box.js
@@ -0,0 +1,154 @@
+/* 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-env browser */
+"use strict";
+
+// A box with a start and a end pane, separated by a dragable splitter that
+// allows the user to resize the relative widths of the panes.
+//
+// +-----------------------+---------------------+
+// | | |
+// | | |
+// | S |
+// | Start Pane p End Pane |
+// | l |
+// | i |
+// | t |
+// | t |
+// | e |
+// | r |
+// | | |
+// | | |
+// +-----------------------+---------------------+
+
+const {
+ DOM: dom,
+ createClass,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+module.exports = createClass({
+ displayName: "HSplitBox",
+
+ propTypes: {
+ // The contents of the start pane.
+ start: PropTypes.any.isRequired,
+
+ // The contents of the end pane.
+ end: PropTypes.any.isRequired,
+
+ // The relative width of the start pane, expressed as a number between 0 and
+ // 1. The relative width of the end pane is 1 - startWidth. For example,
+ // with startWidth = .5, both panes are of equal width; with startWidth =
+ // .25, the start panel will take up 1/4 width and the end panel will take
+ // up 3/4 width.
+ startWidth: PropTypes.number,
+
+ // A minimum css width value for the start and end panes.
+ minStartWidth: PropTypes.any,
+ minEndWidth: PropTypes.any,
+
+ // A callback fired when the user drags the splitter to resize the relative
+ // pane widths. The function is passed the startWidth value that would put
+ // the splitter underneath the users mouse.
+ onResize: PropTypes.func.isRequired,
+ },
+
+ getDefaultProps() {
+ return {
+ startWidth: 0.5,
+ minStartWidth: "20px",
+ minEndWidth: "20px",
+ };
+ },
+
+ getInitialState() {
+ return {
+ mouseDown: false
+ };
+ },
+
+ componentDidMount() {
+ document.defaultView.top.addEventListener("mouseup", this._onMouseUp,
+ false);
+ document.defaultView.top.addEventListener("mousemove", this._onMouseMove,
+ false);
+ },
+
+ componentWillUnmount() {
+ document.defaultView.top.removeEventListener("mouseup", this._onMouseUp,
+ false);
+ document.defaultView.top.removeEventListener("mousemove", this._onMouseMove,
+ false);
+ },
+
+ _onMouseDown(event) {
+ if (event.button !== 0) {
+ return;
+ }
+
+ this.setState({ mouseDown: true });
+ event.preventDefault();
+ },
+
+ _onMouseUp(event) {
+ if (event.button !== 0 || !this.state.mouseDown) {
+ return;
+ }
+
+ this.setState({ mouseDown: false });
+ event.preventDefault();
+ },
+
+ _onMouseMove(event) {
+ if (!this.state.mouseDown) {
+ return;
+ }
+
+ const rect = this.refs.box.getBoundingClientRect();
+ const { left, right } = rect;
+ const width = right - left;
+ const relative = event.clientX - left;
+ this.props.onResize(relative / width);
+
+ event.preventDefault();
+ },
+
+ render() {
+ /* eslint-disable no-shadow */
+ const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
+ assert(startWidth => 0 && startWidth <= 1,
+ "0 <= this.props.startWidth <= 1");
+ /* eslint-enable */
+ return dom.div(
+ {
+ className: "h-split-box",
+ ref: "box",
+ },
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: startWidth, minWidth: minStartWidth },
+ },
+ start
+ ),
+
+ dom.div({
+ className: "devtools-side-splitter",
+ onMouseDown: this._onMouseDown,
+ }),
+
+ dom.div(
+ {
+ className: "h-split-box-pane",
+ style: { flex: 1 - startWidth, minWidth: minEndWidth },
+ },
+ end
+ )
+ );
+ }
+});
diff --git a/devtools/client/shared/components/moz.build b/devtools/client/shared/components/moz.build
new file mode 100644
index 000000000..0d67e90b5
--- /dev/null
+++ b/devtools/client/shared/components/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'reps',
+ 'splitter',
+ 'tabs',
+ 'tree'
+]
+
+DevToolsModules(
+ 'frame.js',
+ 'h-split-box.js',
+ 'notification-box.css',
+ 'notification-box.js',
+ 'search-box.js',
+ 'sidebar-toggle.css',
+ 'sidebar-toggle.js',
+ 'stack-trace.js',
+ 'tree.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
diff --git a/devtools/client/shared/components/notification-box.css b/devtools/client/shared/components/notification-box.css
new file mode 100644
index 000000000..83c29b616
--- /dev/null
+++ b/devtools/client/shared/components/notification-box.css
@@ -0,0 +1,95 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Layout */
+
+.notificationbox .notificationInner {
+ display: flex;
+ flex-direction: row;
+}
+
+.notificationbox .details {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.notificationbox .notification-button {
+ text-align: right;
+}
+
+.notificationbox .messageText {
+ flex-grow: 1;
+}
+
+.notificationbox .details:-moz-dir(rtl)
+.notificationbox .notificationInner:-moz-dir(rtl) {
+ flex-direction: row-reverse;
+}
+
+/* Style */
+
+.notificationbox .notification {
+ background-color: InfoBackground;
+ text-shadow: none;
+ border-top: 1px solid ThreeDShadow;
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+.notificationbox .notification[data-type="info"] {
+ color: -moz-DialogText;
+ background-color: -moz-Dialog;
+}
+
+.notificationbox .notification[data-type="critical"] {
+ color: white;
+ background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0));
+}
+
+.notificationbox .messageImage {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin: 6px;
+}
+
+/* Default icons for notifications */
+
+.notificationbox .messageImage[data-type="info"] {
+ background-image: url("chrome://global/skin/icons/information-16.png");
+}
+
+.notificationbox .messageImage[data-type="warning"] {
+ background-image: url("chrome://global/skin/icons/warning-16.png");
+}
+
+.notificationbox .messageImage[data-type="critical"] {
+ background-image: url("chrome://global/skin/icons/error-16.png");
+}
+
+/* Close button */
+
+.notificationbox .messageCloseButton {
+ width: 20px;
+ height: 20px;
+ margin: 4px;
+ margin-inline-end: 8px;
+ background-image: url("chrome://devtools/skin/images/close.svg");
+ background-position: center;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ border-radius: 11px;
+ filter: invert(0);
+}
+
+.notificationbox .messageCloseButton:hover {
+ background-color: gray;
+ filter: invert(1);
+}
+
+.notificationbox .messageCloseButton:active {
+ background-color: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
+}
diff --git a/devtools/client/shared/components/notification-box.js b/devtools/client/shared/components/notification-box.js
new file mode 100644
index 000000000..87fc76cd6
--- /dev/null
+++ b/devtools/client/shared/components/notification-box.js
@@ -0,0 +1,263 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const Immutable = require("devtools/client/shared/vendor/immutable");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
+
+// Shortcuts
+const { PropTypes, createClass, DOM } = React;
+const { div, span, button } = DOM;
+
+// Priority Levels
+const PriorityLevels = {
+ PRIORITY_INFO_LOW: 1,
+ PRIORITY_INFO_MEDIUM: 2,
+ PRIORITY_INFO_HIGH: 3,
+ PRIORITY_WARNING_LOW: 4,
+ PRIORITY_WARNING_MEDIUM: 5,
+ PRIORITY_WARNING_HIGH: 6,
+ PRIORITY_CRITICAL_LOW: 7,
+ PRIORITY_CRITICAL_MEDIUM: 8,
+ PRIORITY_CRITICAL_HIGH: 9,
+ PRIORITY_CRITICAL_BLOCK: 10,
+};
+
+/**
+ * This component represents Notification Box - HTML alternative for
+ * <xul:notifictionbox> binding.
+ *
+ * See also MDN for more info about <xul:notificationbox>:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
+ */
+var NotificationBox = createClass({
+ displayName: "NotificationBox",
+
+ propTypes: {
+ // List of notifications appended into the box.
+ notifications: PropTypes.arrayOf(PropTypes.shape({
+ // label to appear on the notification.
+ label: PropTypes.string.isRequired,
+
+ // Value used to identify the notification
+ value: PropTypes.string.isRequired,
+
+ // URL of image to appear on the notification. If "" then an icon
+ // appropriate for the priority level is used.
+ image: PropTypes.string.isRequired,
+
+ // Notification priority; see Priority Levels.
+ priority: PropTypes.number.isRequired,
+
+ // Array of button descriptions to appear on the notification.
+ buttons: PropTypes.arrayOf(PropTypes.shape({
+ // Function to be called when the button is activated.
+ // This function is passed three arguments:
+ // 1) the NotificationBox component the button is associated with
+ // 2) the button description as passed to appendNotification.
+ // 3) the element which was the target of the button press event.
+ // If the return value from this function is not True, then the
+ // notification is closed. The notification is also not closed
+ // if an error is thrown.
+ callback: PropTypes.func.isRequired,
+
+ // The label to appear on the button.
+ label: PropTypes.string.isRequired,
+
+ // The accesskey attribute set on the <button> element.
+ accesskey: PropTypes.string,
+ })),
+
+ // A function to call to notify you of interesting things that happen
+ // with the notification box.
+ eventCallback: PropTypes.func,
+ })),
+
+ // Message that should be shown when hovering over the close button
+ closeButtonTooltip: PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip")
+ };
+ },
+
+ getInitialState() {
+ return {
+ notifications: new Immutable.OrderedMap()
+ };
+ },
+
+ /**
+ * Create a new notification and display it. If another notification is
+ * already present with a higher priority, the new notification will be
+ * added behind it. See `propTypes` for arguments description.
+ */
+ appendNotification(label, value, image, priority, buttons = [],
+ eventCallback) {
+ // Priority level must be within expected interval
+ // (see priority levels at the top of this file).
+ if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
+ priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
+ throw new Error("Invalid notification priority " + priority);
+ }
+
+ // Custom image URL is not supported yet.
+ if (image) {
+ throw new Error("Custom image URL is not supported yet");
+ }
+
+ let type = "warning";
+ if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
+ type = "critical";
+ } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
+ type = "info";
+ }
+
+ let notifications = this.state.notifications.set(value, {
+ label: label,
+ value: value,
+ image: image,
+ priority: priority,
+ type: type,
+ buttons: buttons,
+ eventCallback: eventCallback,
+ });
+
+ // High priorities must be on top.
+ notifications = notifications.sortBy((val, key) => {
+ return -val.priority;
+ });
+
+ this.setState({
+ notifications: notifications
+ });
+ },
+
+ /**
+ * Remove specific notification from the list.
+ */
+ removeNotification(notification) {
+ this.close(this.state.notifications.get(notification.value));
+ },
+
+ /**
+ * Returns an object that represents a notification. It can be
+ * used to close it.
+ */
+ getNotificationWithValue(value) {
+ let notification = this.state.notifications.get(value);
+ if (!notification) {
+ return null;
+ }
+
+ // Return an object that can be used to remove the notification
+ // later (using `removeNotification` method) or directly close it.
+ return Object.assign({}, notification, {
+ close: () => {
+ this.close(notification);
+ }
+ });
+ },
+
+ getCurrentNotification() {
+ return this.state.notifications.first();
+ },
+
+ /**
+ * Close specified notification.
+ */
+ close(notification) {
+ if (!notification) {
+ return;
+ }
+
+ if (notification.eventCallback) {
+ notification.eventCallback("removed");
+ }
+
+ this.setState({
+ notifications: this.state.notifications.remove(notification.value)
+ });
+ },
+
+ /**
+ * Render a button. A notification can have a set of custom buttons.
+ * These are used to execute custom callback.
+ */
+ renderButton(props, notification) {
+ let onClick = event => {
+ if (props.callback) {
+ let result = props.callback(this, props, event.target);
+ if (!result) {
+ this.close(notification);
+ }
+ event.stopPropagation();
+ }
+ };
+
+ return (
+ button({
+ key: props.label,
+ className: "notification-button",
+ accesskey: props.accesskey,
+ onClick: onClick},
+ props.label
+ )
+ );
+ },
+
+ /**
+ * Render a notification.
+ */
+ renderNotification(notification) {
+ return (
+ div({
+ key: notification.value,
+ className: "notification",
+ "data-type": notification.type},
+ div({className: "notificationInner"},
+ div({className: "details"},
+ div({
+ className: "messageImage",
+ "data-type": notification.type}),
+ span({className: "messageText"},
+ notification.label
+ ),
+ notification.buttons.map(props =>
+ this.renderButton(props, notification)
+ )
+ ),
+ div({
+ className: "messageCloseButton",
+ title: this.props.closeButtonTooltip,
+ onClick: this.close.bind(this, notification)}
+ )
+ )
+ )
+ );
+ },
+
+ /**
+ * Render the top (highest priority) notification. Only one
+ * notification is rendered at a time.
+ */
+ render() {
+ let notification = this.state.notifications.first();
+ let content = notification ?
+ this.renderNotification(notification) :
+ null;
+
+ return div({className: "notificationbox"},
+ content
+ );
+ },
+});
+
+module.exports.NotificationBox = NotificationBox;
+module.exports.PriorityLevels = PriorityLevels;
diff --git a/devtools/client/shared/components/reps/array.js b/devtools/client/shared/components/reps/array.js
new file mode 100644
index 000000000..8ec1443e1
--- /dev/null
+++ b/devtools/client/shared/components/reps/array.js
@@ -0,0 +1,186 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ let ArrayRep = React.createClass({
+ displayName: "ArrayRep",
+
+ getTitle: function (object, context) {
+ return "[" + object.length + "]";
+ },
+
+ arrayIterator: function (array, max) {
+ let items = [];
+ let delim;
+
+ for (let i = 0; i < array.length && i < max; i++) {
+ try {
+ let value = array[i];
+
+ delim = (i == array.length - 1 ? "" : ", ");
+
+ items.push(ItemRep({
+ object: value,
+ // Hardcode tiny mode to avoid recursive handling.
+ mode: "tiny",
+ delim: delim
+ }));
+ } catch (exc) {
+ items.push(ItemRep({
+ object: exc,
+ mode: "tiny",
+ delim: delim
+ }));
+ }
+ }
+
+ if (array.length > max) {
+ let objectLink = this.props.objectLink || DOM.span;
+ items.push(Caption({
+ object: objectLink({
+ object: this.props.object
+ }, (array.length - max) + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ /**
+ * Returns true if the passed object is an array with additional (custom)
+ * properties, otherwise returns false. Custom properties should be
+ * displayed in extra expandable section.
+ *
+ * Example array with a custom property.
+ * let arr = [0, 1];
+ * arr.myProp = "Hello";
+ *
+ * @param {Array} array The array object.
+ */
+ hasSpecialProperties: function (array) {
+ function isInteger(x) {
+ let y = parseInt(x, 10);
+ if (isNaN(y)) {
+ return false;
+ }
+ return x === y.toString();
+ }
+
+ let props = Object.getOwnPropertyNames(array);
+ for (let i = 0; i < props.length; i++) {
+ let p = props[i];
+
+ // Valid indexes are skipped
+ if (isInteger(p)) {
+ continue;
+ }
+
+ // Ignore standard 'length' property, anything else is custom.
+ if (p != "length") {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ // Event Handlers
+
+ onToggleProperties: function (event) {
+ },
+
+ onClickBracket: function (event) {
+ },
+
+ render: function () {
+ let mode = this.props.mode || "short";
+ let object = this.props.object;
+ let items;
+ let brackets;
+ let needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]"} : { left: "[", right: "]"};
+ };
+
+ if (mode == "tiny") {
+ let isEmpty = object.length === 0;
+ items = [DOM.span({className: "length"}, isEmpty ? "" : object.length)];
+ brackets = needSpace(false);
+ } else {
+ let max = (mode == "short") ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ let objectLink = this.props.objectLink || DOM.span;
+
+ return (
+ DOM.span({
+ className: "objectBox objectBox-array"},
+ objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left),
+ ...items,
+ objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right),
+ DOM.span({
+ className: "arrayProperties",
+ role: "group"}
+ )
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders array item. Individual values are separated by a comma.
+ */
+ let ItemRep = React.createFactory(React.createClass({
+ displayName: "ItemRep",
+
+ render: function () {
+ const { Rep } = createFactories(require("./rep"));
+
+ let object = this.props.object;
+ let delim = this.props.delim;
+ let mode = this.props.mode;
+ return (
+ DOM.span({},
+ Rep({object: object, mode: mode}),
+ delim
+ )
+ );
+ }
+ }));
+
+ function supportsObject(object, type) {
+ return Array.isArray(object) ||
+ Object.prototype.toString.call(object) === "[object Arguments]";
+ }
+
+ // Exports from this module
+ exports.ArrayRep = {
+ rep: ArrayRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/attribute.js b/devtools/client/shared/components/reps/attribute.js
new file mode 100644
index 000000000..f57ed0380
--- /dev/null
+++ b/devtools/client/shared/components/reps/attribute.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { StringRep } = require("./string");
+
+ // Shortcuts
+ const { span } = React.DOM;
+ const { rep: StringRepFactory } = createFactories(StringRep);
+
+ /**
+ * Renders DOM attribute
+ */
+ let Attribute = React.createClass({
+ displayName: "Attr",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ return grip.preview.nodeName;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let value = grip.preview.value;
+ let objectLink = this.props.objectLink || span;
+
+ return (
+ objectLink({className: "objectLink-Attr"},
+ span({},
+ span({className: "attrTitle"},
+ this.getTitle(grip)
+ ),
+ span({className: "attrEqual"},
+ "="
+ ),
+ StringRepFactory({object: value})
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (type == "Attr" && grip.preview);
+ }
+
+ exports.Attribute = {
+ rep: Attribute,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/caption.js b/devtools/client/shared/components/reps/caption.js
new file mode 100644
index 000000000..7f00b01e8
--- /dev/null
+++ b/devtools/client/shared/components/reps/caption.js
@@ -0,0 +1,31 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const DOM = React.DOM;
+
+ /**
+ * Renders a caption. This template is used by other components
+ * that needs to distinguish between a simple text/value and a label.
+ */
+ const Caption = React.createClass({
+ displayName: "Caption",
+
+ render: function () {
+ return (
+ DOM.span({"className": "caption"}, this.props.object)
+ );
+ },
+ });
+
+ // Exports from this module
+ exports.Caption = Caption;
+});
diff --git a/devtools/client/shared/components/reps/comment-node.js b/devtools/client/shared/components/reps/comment-node.js
new file mode 100644
index 000000000..2c69c1414
--- /dev/null
+++ b/devtools/client/shared/components/reps/comment-node.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const { isGrip, cropString, cropMultipleLines } = require("./rep-utils");
+
+ // Utils
+ const nodeConstants = require("devtools/shared/dom-node-constants");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM comment node.
+ */
+ const CommentNode = React.createClass({
+ displayName: "CommentNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ render: function () {
+ let {object} = this.props;
+
+ let mode = this.props.mode || "short";
+
+ let {textContent} = object.preview;
+ if (mode === "tiny") {
+ textContent = cropMultipleLines(textContent, 30);
+ } else if (mode === "short") {
+ textContent = cropString(textContent, 50);
+ }
+
+ return span({className: "objectBox theme-comment"}, `<!-- ${textContent} -->`);
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.nodeType === nodeConstants.COMMENT_NODE;
+ }
+
+ // Exports from this module
+ exports.CommentNode = {
+ rep: CommentNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/date-time.js b/devtools/client/shared/components/reps/date-time.js
new file mode 100644
index 000000000..55dfb7d2d
--- /dev/null
+++ b/devtools/client/shared/components/reps/date-time.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Used to render JS built-in Date() object.
+ */
+ let DateTime = React.createClass({
+ displayName: "Date",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, grip.class + " ");
+ }
+ return "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let date;
+ try {
+ date = span({className: "objectBox"},
+ this.getTitle(grip),
+ span({className: "Date"},
+ new Date(grip.preview.timestamp).toISOString()
+ )
+ );
+ } catch (e) {
+ date = span({className: "objectBox"}, "Invalid Date");
+ }
+ return date;
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (type == "Date" && grip.preview);
+ }
+
+ // Exports from this module
+ exports.DateTime = {
+ rep: DateTime,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/document.js b/devtools/client/shared/components/reps/document.js
new file mode 100644
index 000000000..25e42609f
--- /dev/null
+++ b/devtools/client/shared/components/reps/document.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM document object.
+ */
+ let Document = React.createClass({
+ displayName: "Document",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getLocation: function (grip) {
+ let location = grip.preview.location;
+ return location ? getURLDisplayString(location) : "";
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, grip.class + " ")
+ );
+ }
+ return "";
+ },
+
+ getTooltip: function (doc) {
+ return doc.location.href;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (object.preview && type == "HTMLDocument");
+ }
+
+ // Exports from this module
+ exports.Document = {
+ rep: Document,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/element-node.js b/devtools/client/shared/components/reps/element-node.js
new file mode 100644
index 000000000..6315fb5b1
--- /dev/null
+++ b/devtools/client/shared/components/reps/element-node.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const { isGrip } = require("./rep-utils");
+
+ // Utils
+ const nodeConstants = require("devtools/shared/dom-node-constants");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders DOM element node.
+ */
+ const ElementNode = React.createClass({
+ displayName: "ElementNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getElements: function (grip, mode) {
+ let {attributes, nodeName} = grip.preview;
+ const nodeNameElement = span({
+ className: "tag-name theme-fg-color3"
+ }, nodeName);
+
+ if (mode === "tiny") {
+ let elements = [nodeNameElement];
+ if (attributes.id) {
+ elements.push(
+ span({className: "attr-name theme-fg-color2"}, `#${attributes.id}`));
+ }
+ if (attributes.class) {
+ elements.push(
+ span({className: "attr-name theme-fg-color2"},
+ attributes.class
+ .replace(/(^\s+)|(\s+$)/g, "")
+ .split(" ")
+ .map(cls => `.${cls}`)
+ .join("")
+ )
+ );
+ }
+ return elements;
+ }
+ let attributeElements = Object.keys(attributes)
+ .sort(function getIdAndClassFirst(a1, a2) {
+ if ([a1, a2].includes("id")) {
+ return 3 * (a1 === "id" ? -1 : 1);
+ }
+ if ([a1, a2].includes("class")) {
+ return 2 * (a1 === "class" ? -1 : 1);
+ }
+
+ // `id` and `class` excepted,
+ // we want to keep the same order that in `attributes`.
+ return 0;
+ })
+ .reduce((arr, name, i, keys) => {
+ let value = attributes[name];
+ let attribute = span({},
+ span({className: "attr-name theme-fg-color2"}, `${name}`),
+ `="`,
+ span({className: "attr-value theme-fg-color6"}, `${value}`),
+ `"`
+ );
+
+ return arr.concat([" ", attribute]);
+ }, []);
+
+ return [
+ "<",
+ nodeNameElement,
+ ...attributeElements,
+ ">",
+ ];
+ },
+
+ render: function () {
+ let {object, mode} = this.props;
+ let elements = this.getElements(object, mode);
+ const baseElement = span({className: "objectBox"}, ...elements);
+
+ if (this.props.objectLink) {
+ return this.props.objectLink({object}, baseElement);
+ }
+ return baseElement;
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.preview && object.preview.nodeType === nodeConstants.ELEMENT_NODE;
+ }
+
+ // Exports from this module
+ exports.ElementNode = {
+ rep: ElementNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/event.js b/devtools/client/shared/components/reps/event.js
new file mode 100644
index 000000000..1d37e0150
--- /dev/null
+++ b/devtools/client/shared/components/reps/event.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { rep } = createFactories(require("./grip").Grip);
+
+ /**
+ * Renders DOM event objects.
+ */
+ let Event = React.createClass({
+ displayName: "event",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ // Use `Object.assign` to keep `this.props` without changes because:
+ // 1. JSON.stringify/JSON.parse is slow.
+ // 2. Immutable.js is planned for the future.
+ let props = Object.assign({}, this.props);
+ props.object = Object.assign({}, this.props.object);
+ props.object.preview = Object.assign({}, this.props.object.preview);
+ props.object.preview.ownProperties = props.object.preview.properties;
+ delete props.object.preview.properties;
+ props.object.ownPropertyLength =
+ Object.keys(props.object.preview.ownProperties).length;
+
+ switch (props.object.class) {
+ case "MouseEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "clientX" ||
+ name == "clientY" ||
+ name == "layerX" ||
+ name == "layerY");
+ };
+ break;
+ case "KeyboardEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "key" ||
+ name == "charCode" ||
+ name == "keyCode");
+ };
+ break;
+ case "MessageEvent":
+ props.isInterestingProp = (type, value, name) => {
+ return (name == "isTrusted" ||
+ name == "data");
+ };
+ break;
+ }
+ return rep(props);
+ }
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "DOMEvent");
+ }
+
+ // Exports from this module
+ exports.Event = {
+ rep: Event,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/function.js b/devtools/client/shared/components/reps/function.js
new file mode 100644
index 000000000..fd20dc318
--- /dev/null
+++ b/devtools/client/shared/components/reps/function.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * This component represents a template for Function objects.
+ */
+ let Func = React.createClass({
+ displayName: "Func",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "function ");
+ }
+ return "";
+ },
+
+ summarizeFunction: function (grip) {
+ let name = grip.userDisplayName || grip.displayName || grip.name || "function";
+ return cropString(name + "()", 100);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ // Set dir="ltr" to prevent function parentheses from
+ // appearing in the wrong direction
+ span({dir: "ltr", className: "objectBox objectBox-function"},
+ this.getTitle(grip),
+ this.summarizeFunction(grip)
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return (type == "function");
+ }
+
+ return (type == "Function");
+ }
+
+ // Exports from this module
+
+ exports.Func = {
+ rep: Func,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip-array.js b/devtools/client/shared/components/reps/grip-array.js
new file mode 100644
index 000000000..04a5603bb
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -0,0 +1,198 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders an array. The array is enclosed by left and right bracket
+ * and the max number of rendered items depends on the current mode.
+ */
+ let GripArray = React.createClass({
+ displayName: "GripArray",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ provider: React.PropTypes.object,
+ },
+
+ getLength: function (grip) {
+ if (!grip.preview) {
+ return 0;
+ }
+
+ return grip.preview.length || grip.preview.childNodesLength || 0;
+ },
+
+ getTitle: function (object, context) {
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode != "tiny") {
+ return objectLink({
+ object: object
+ }, object.class + " ");
+ }
+ return "";
+ },
+
+ getPreviewItems: function (grip) {
+ if (!grip.preview) {
+ return null;
+ }
+
+ return grip.preview.items || grip.preview.childNodes || null;
+ },
+
+ arrayIterator: function (grip, max) {
+ let items = [];
+ const gripLength = this.getLength(grip);
+
+ if (!gripLength) {
+ return items;
+ }
+
+ const previewItems = this.getPreviewItems(grip);
+ if (!previewItems) {
+ return items;
+ }
+
+ let delim;
+ // number of grip preview items is limited to 10, but we may have more
+ // items in grip-array.
+ let delimMax = gripLength > previewItems.length ?
+ previewItems.length : previewItems.length - 1;
+ let provider = this.props.provider;
+
+ for (let i = 0; i < previewItems.length && i < max; i++) {
+ try {
+ let itemGrip = previewItems[i];
+ let value = provider ? provider.getValue(itemGrip) : itemGrip;
+
+ delim = (i == delimMax ? "" : ", ");
+
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: value,
+ delim: delim
+ })));
+ } catch (exc) {
+ items.push(GripArrayItem(Object.assign({}, this.props, {
+ object: exc,
+ delim: delim
+ })));
+ }
+ }
+ if (previewItems.length > max || gripLength > previewItems.length) {
+ let objectLink = this.props.objectLink || span;
+ let leftItemNum = gripLength - max > 0 ?
+ gripLength - max : gripLength - previewItems.length;
+ items.push(Caption({
+ object: objectLink({
+ object: this.props.object
+ }, leftItemNum + " more…")
+ }));
+ }
+
+ return items;
+ },
+
+ render: function () {
+ let mode = this.props.mode || "short";
+ let object = this.props.object;
+
+ let items;
+ let brackets;
+ let needSpace = function (space) {
+ return space ? { left: "[ ", right: " ]"} : { left: "[", right: "]"};
+ };
+
+ if (mode == "tiny") {
+ let objectLength = this.getLength(object);
+ let isEmpty = objectLength === 0;
+ items = [span({className: "length"}, isEmpty ? "" : objectLength)];
+ brackets = needSpace(false);
+ } else {
+ let max = (mode == "short") ? 3 : 300;
+ items = this.arrayIterator(object, max);
+ brackets = needSpace(items.length > 0);
+ }
+
+ let objectLink = this.props.objectLink || span;
+ let title = this.getTitle(object);
+
+ return (
+ span({
+ className: "objectBox objectBox-array"},
+ title,
+ objectLink({
+ className: "arrayLeftBracket",
+ object: object
+ }, brackets.left),
+ ...items,
+ objectLink({
+ className: "arrayRightBracket",
+ object: object
+ }, brackets.right),
+ span({
+ className: "arrayProperties",
+ role: "group"}
+ )
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders array item. Individual values are separated by
+ * a delimiter (a comma by default).
+ */
+ let GripArrayItem = React.createFactory(React.createClass({
+ displayName: "GripArrayItem",
+
+ propTypes: {
+ delim: React.PropTypes.string,
+ },
+
+ render: function () {
+ let { Rep } = createFactories(require("./rep"));
+
+ return (
+ span({},
+ Rep(Object.assign({}, this.props, {
+ mode: "tiny"
+ })),
+ this.props.delim
+ )
+ );
+ }
+ }));
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && (
+ grip.preview.kind == "ArrayLike" ||
+ type === "DocumentFragment"
+ )
+ );
+ }
+
+ // Exports from this module
+ exports.GripArray = {
+ rep: GripArray,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip-map.js b/devtools/client/shared/components/reps/grip-map.js
new file mode 100644
index 000000000..df673d005
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-map.js
@@ -0,0 +1,193 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+ const GripMap = React.createClass({
+ displayName: "GripMap",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let title = object && object.class ? object.class : "Map";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ safeEntriesIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.entriesIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ entriesIterator: function (object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ let isInterestingEntry = this.props.isInterestingEntry || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let mapEntries = object.preview && object.preview.entries
+ ? object.preview.entries : [];
+
+ let indexes = this.getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(
+ this.getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ })
+ );
+ }
+
+ let entries = this.getEntries(mapEntries, indexes);
+ if (entries.length < mapEntries.length) {
+ // There are some undisplayed entries. Then display "more…".
+ let objectLink = this.props.objectLink || span;
+
+ entries.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, `${mapEntries.length - max} more…`)
+ }));
+ }
+
+ return entries;
+ },
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ getEntries: function (entries, indexes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ let [key, entryValue] = entries[index];
+ let value = entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ // key,
+ name: key,
+ equal: ": ",
+ object: value,
+ // Do not add a trailing comma on the last entry
+ // if there won't be a "more..." item.
+ delim: (i < indexes.length - 1 || indexes.length < entries.length) ? ", " : "",
+ mode: "tiny",
+ objectLink: this.props.objectLink,
+ });
+ });
+ },
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ getEntriesIndexes: function (entries, max, filter) {
+ return entries
+ .reduce((indexes, [key, entry], i) => {
+ if (indexes.length < max) {
+ let value = (entry && entry.value !== undefined) ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let type = (value && value.class ? value.class : typeof value).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safeEntriesIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+ return (grip.preview && grip.preview.kind == "MapLike");
+ }
+
+ // Exports from this module
+ exports.GripMap = {
+ rep: GripMap,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/grip.js b/devtools/client/shared/components/reps/grip.js
new file mode 100644
index 000000000..c63ee19f3
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip.js
@@ -0,0 +1,247 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ // Dependencies
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders generic grip. Grip is client representation
+ * of remote JS object and is used as an input object
+ * for this rep component.
+ */
+ const GripRep = React.createClass({
+ displayName: "Grip",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ isInterestingProp: React.PropTypes.func
+ },
+
+ getTitle: function (object) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, object.class);
+ }
+ return object.class || "Object";
+ },
+
+ safePropIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
+ const { Rep } = createFactories(require("./rep"));
+
+ return [Rep({
+ object: object.preview.wrappedValue,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ })];
+ }
+
+ // Property filter. Show only interesting properties to the user.
+ let isInterestingProp = this.props.isInterestingProp || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let properties = object.preview
+ ? object.preview.ownProperties
+ : {};
+ let propertiesLength = object.preview && object.preview.ownPropertiesLength
+ ? object.preview.ownPropertiesLength
+ : object.ownPropertyLength;
+
+ if (object.preview && object.preview.safeGetterValues) {
+ properties = Object.assign({}, properties, object.preview.safeGetterValues);
+ propertiesLength += Object.keys(object.preview.safeGetterValues).length;
+ }
+
+ let indexes = this.getPropIndexes(properties, max, isInterestingProp);
+ if (indexes.length < max && indexes.length < propertiesLength) {
+ // There are not enough props yet. Then add uninteresting props to display them.
+ indexes = indexes.concat(
+ this.getPropIndexes(properties, max - indexes.length, (t, value, name) => {
+ return !isInterestingProp(t, value, name);
+ })
+ );
+ }
+
+ const truncate = Object.keys(properties).length > max;
+ let props = this.getProps(properties, indexes, truncate);
+ if (truncate) {
+ // There are some undisplayed props. Then display "more...".
+ let objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ object: objectLink({
+ object: object
+ }, `${object.ownPropertyLength - max} more…`)
+ }));
+ }
+
+ return props;
+ },
+
+ /**
+ * Get props ordered by index.
+ *
+ * @param {Object} properties Props object.
+ * @param {Array} indexes Indexes of props.
+ * @param {Boolean} truncate true if the grip will be truncated.
+ * @return {Array} Props.
+ */
+ getProps: function (properties, indexes, truncate) {
+ let props = [];
+
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ indexes.forEach((i) => {
+ let name = Object.keys(properties)[i];
+ let value = this.getPropValue(properties[name]);
+
+ props.push(PropRep(Object.assign({}, this.props, {
+ mode: "tiny",
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: i !== indexes.length - 1 || truncate ? ", " : "",
+ defaultRep: Grip
+ })));
+ });
+
+ return props;
+ },
+
+ /**
+ * Get the indexes of props in the object.
+ *
+ * @param {Object} properties Props object.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the props you want.
+ * @return {Array} Indexes of interesting props in the object.
+ */
+ getPropIndexes: function (properties, max, filter) {
+ let indexes = [];
+
+ try {
+ let i = 0;
+ for (let name in properties) {
+ if (indexes.length >= max) {
+ return indexes;
+ }
+
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let value = this.getPropValue(properties[name]);
+ let type = (value.class || typeof value);
+ type = type.toLowerCase();
+
+ if (filter(type, value, name)) {
+ indexes.push(i);
+ }
+ i++;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ return indexes;
+ },
+
+ /**
+ * Get the actual value of a property.
+ *
+ * @param {Object} property
+ * @return {Object} Value of the property.
+ */
+ getPropValue: function (property) {
+ let value = property;
+ if (typeof property === "object") {
+ let keys = Object.keys(property);
+ if (keys.includes("value")) {
+ value = property.value;
+ } else if (keys.includes("getterValue")) {
+ value = property.getterValue;
+ }
+ }
+ return value;
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safePropIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return (object.preview && object.preview.ownProperties);
+ }
+
+ let Grip = {
+ rep: GripRep,
+ supportsObject: supportsObject
+ };
+
+ // Exports from this module
+ exports.Grip = Grip;
+});
diff --git a/devtools/client/shared/components/reps/infinity.js b/devtools/client/shared/components/reps/infinity.js
new file mode 100644
index 000000000..604e31f06
--- /dev/null
+++ b/devtools/client/shared/components/reps/infinity.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a Infinity object
+ */
+ const InfinityRep = React.createClass({
+ displayName: "Infinity",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-number"},
+ this.props.object.type
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "Infinity" || type == "-Infinity";
+ }
+
+ // Exports from this module
+ exports.InfinityRep = {
+ rep: InfinityRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/long-string.js b/devtools/client/shared/components/reps/long-string.js
new file mode 100644
index 000000000..f19f020dc
--- /dev/null
+++ b/devtools/client/shared/components/reps/long-string.js
@@ -0,0 +1,71 @@
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { sanitizeString, isGrip } = require("./rep-utils");
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a long string grip.
+ */
+ const LongStringRep = React.createClass({
+ displayName: "LongStringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool,
+ style: React.PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true,
+ };
+ },
+
+ render: function () {
+ let {
+ cropLimit,
+ member,
+ object,
+ style,
+ useQuotes
+ } = this.props;
+ let {fullText, initial, length} = object;
+
+ let config = {className: "objectBox objectBox-string"};
+ if (style) {
+ config.style = style;
+ }
+
+ let string = member && member.open
+ ? fullText || initial
+ : initial.substring(0, cropLimit);
+
+ if (string.length < length) {
+ string += "\u2026";
+ }
+ let formattedString = useQuotes ? `"${string}"` : string;
+ return span(config, sanitizeString(formattedString));
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return object.type === "longString";
+ }
+
+ // Exports from this module
+ exports.LongStringRep = {
+ rep: LongStringRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/moz.build b/devtools/client/shared/components/reps/moz.build
new file mode 100644
index 000000000..f5df589f7
--- /dev/null
+++ b/devtools/client/shared/components/reps/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'array.js',
+ 'attribute.js',
+ 'caption.js',
+ 'comment-node.js',
+ 'date-time.js',
+ 'document.js',
+ 'element-node.js',
+ 'event.js',
+ 'function.js',
+ 'grip-array.js',
+ 'grip-map.js',
+ 'grip.js',
+ 'infinity.js',
+ 'long-string.js',
+ 'nan.js',
+ 'null.js',
+ 'number.js',
+ 'object-with-text.js',
+ 'object-with-url.js',
+ 'object.js',
+ 'promise.js',
+ 'prop-rep.js',
+ 'regexp.js',
+ 'rep-utils.js',
+ 'rep.js',
+ 'reps.css',
+ 'string.js',
+ 'stylesheet.js',
+ 'symbol.js',
+ 'text-node.js',
+ 'undefined.js',
+ 'window.js',
+)
diff --git a/devtools/client/shared/components/reps/nan.js b/devtools/client/shared/components/reps/nan.js
new file mode 100644
index 000000000..b76a5cfd3
--- /dev/null
+++ b/devtools/client/shared/components/reps/nan.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a NaN object
+ */
+ const NaNRep = React.createClass({
+ displayName: "NaN",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-nan"},
+ "NaN"
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return type == "NaN";
+ }
+
+ // Exports from this module
+ exports.NaNRep = {
+ rep: NaNRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/null.js b/devtools/client/shared/components/reps/null.js
new file mode 100644
index 000000000..5de00f026
--- /dev/null
+++ b/devtools/client/shared/components/reps/null.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders null value
+ */
+ const Null = React.createClass({
+ displayName: "NullRep",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-null"},
+ "null"
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "null") {
+ return true;
+ }
+
+ return (object == null);
+ }
+
+ // Exports from this module
+
+ exports.Null = {
+ rep: Null,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/number.js b/devtools/client/shared/components/reps/number.js
new file mode 100644
index 000000000..31be3009b
--- /dev/null
+++ b/devtools/client/shared/components/reps/number.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a number
+ */
+ const Number = React.createClass({
+ displayName: "Number",
+
+ stringify: function (object) {
+ let isNegativeZero = Object.is(object, -0) ||
+ (object.type && object.type == "-0");
+
+ return (isNegativeZero ? "-0" : String(object));
+ },
+
+ render: function () {
+ let value = this.props.object;
+
+ return (
+ span({className: "objectBox objectBox-number"},
+ this.stringify(value)
+ )
+ );
+ }
+ });
+
+ function supportsObject(object, type) {
+ return ["boolean", "number", "-0"].includes(type);
+ }
+
+ // Exports from this module
+
+ exports.Number = {
+ rep: Number,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object-with-text.js b/devtools/client/shared/components/reps/object-with-text.js
new file mode 100644
index 000000000..85168ce78
--- /dev/null
+++ b/devtools/client/shared/components/reps/object-with-text.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with textual data.
+ */
+ let ObjectWithText = React.createClass({
+ displayName: "ObjectWithText",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " ")
+ );
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return "\"" + grip.preview.text + "\"";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ return (
+ span({className: "objectBox objectBox-" + this.getType(grip)},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getDescription(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "ObjectWithText");
+ }
+
+ // Exports from this module
+ exports.ObjectWithText = {
+ rep: ObjectWithText,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object-with-url.js b/devtools/client/shared/components/reps/object-with-url.js
new file mode 100644
index 000000000..9c4b9a229
--- /dev/null
+++ b/devtools/client/shared/components/reps/object-with-url.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with URL data.
+ */
+ let ObjectWithURL = React.createClass({
+ displayName: "ObjectWithURL",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, this.getType(grip) + " ")
+ );
+ }
+ return "";
+ },
+
+ getType: function (grip) {
+ return grip.class;
+ },
+
+ getDescription: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ return (
+ span({className: "objectBox objectBox-" + this.getType(grip)},
+ this.getTitle(grip),
+ span({className: "objectPropValue"},
+ this.getDescription(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.preview.kind == "ObjectWithURL");
+ }
+
+ // Exports from this module
+ exports.ObjectWithURL = {
+ rep: ObjectWithURL,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/object.js b/devtools/client/shared/components/reps/object.js
new file mode 100644
index 000000000..ffb1d1525
--- /dev/null
+++ b/devtools/client/shared/components/reps/object.js
@@ -0,0 +1,171 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an object. An object is represented by a list of its
+ * properties enclosed in curly brackets.
+ */
+ const Obj = React.createClass({
+ displayName: "Obj",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let className = object && object.class ? object.class : "Object";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, className);
+ }
+ return className;
+ },
+
+ safePropIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.propIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ propIterator: function (object, max) {
+ let isInterestingProp = (t, value) => {
+ // Do not pick objects, it could cause recursion.
+ return (t == "boolean" || t == "number" || (t == "string" && value));
+ };
+
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+ if (Object.prototype.toString.call(object) === "[object Generator]") {
+ object = Object.getPrototypeOf(object);
+ }
+
+ // Object members with non-empty values are preferred since it gives the
+ // user a better overview of the object.
+ let props = this.getProps(object, max, isInterestingProp);
+
+ if (props.length <= max) {
+ // There are not enough props yet (or at least, not enough props to
+ // be able to know whether we should print "more…" or not).
+ // Let's display also empty members and functions.
+ props = props.concat(this.getProps(object, max, (t, value) => {
+ return !isInterestingProp(t, value);
+ }));
+ }
+
+ if (props.length > max) {
+ props.pop();
+ let objectLink = this.props.objectLink || span;
+
+ props.push(Caption({
+ object: objectLink({
+ object: object
+ }, (Object.keys(object).length - max) + " more…")
+ }));
+ } else if (props.length > 0) {
+ // Remove the last comma.
+ props[props.length - 1] = React.cloneElement(
+ props[props.length - 1], { delim: "" });
+ }
+
+ return props;
+ },
+
+ getProps: function (object, max, filter) {
+ let props = [];
+
+ max = max || 3;
+ if (!object) {
+ return props;
+ }
+
+ // Hardcode tiny mode to avoid recursive handling.
+ let mode = "tiny";
+
+ try {
+ for (let name in object) {
+ if (props.length > max) {
+ return props;
+ }
+
+ let value;
+ try {
+ value = object[name];
+ } catch (exc) {
+ continue;
+ }
+
+ let t = typeof value;
+ if (filter(t, value)) {
+ props.push(PropRep({
+ mode: mode,
+ name: name,
+ object: value,
+ equal: ": ",
+ delim: ", ",
+ }));
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return props;
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safePropIterator(object);
+ let objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny" || !props.length) {
+ return (
+ span({className: "objectBox objectBox-object"},
+ objectLink({className: "objectTitle"}, this.getTitle(object))
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+ function supportsObject(object, type) {
+ return true;
+ }
+
+ // Exports from this module
+ exports.Obj = {
+ rep: Obj,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/promise.js b/devtools/client/shared/components/reps/promise.js
new file mode 100644
index 000000000..0a903d366
--- /dev/null
+++ b/devtools/client/shared/components/reps/promise.js
@@ -0,0 +1,111 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ // Dependencies
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { PropRep } = createFactories(require("./prop-rep"));
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a DOM Promise object.
+ */
+ const PromiseRep = React.createClass({
+ displayName: "Promise",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ const title = object.class;
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ getProps: function (promiseState) {
+ const keys = ["state"];
+ if (Object.keys(promiseState).includes("value")) {
+ keys.push("value");
+ }
+
+ return keys.map((key, i) => {
+ return PropRep(Object.assign({}, this.props, {
+ mode: "tiny",
+ name: `<${key}>`,
+ object: promiseState[key],
+ equal: ": ",
+ delim: i < keys.length - 1 ? ", " : ""
+ }));
+ });
+ },
+
+ render: function () {
+ const object = this.props.object;
+ const {promiseState} = object;
+ let objectLink = this.props.objectLink || span;
+
+ if (this.props.mode == "tiny") {
+ let { Rep } = createFactories(require("./rep"));
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ Rep({object: promiseState.state}),
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ }
+
+ const props = this.getProps(promiseState);
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ ...props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ // Registration
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+ return type === "Promise";
+ }
+
+ // Exports from this module
+ exports.PromiseRep = {
+ rep: PromiseRep,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/prop-rep.js b/devtools/client/shared/components/reps/prop-rep.js
new file mode 100644
index 000000000..775dfea2b
--- /dev/null
+++ b/devtools/client/shared/components/reps/prop-rep.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories } = require("./rep-utils");
+ const { span } = React.DOM;
+
+ /**
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
+ */
+ let PropRep = React.createFactory(React.createClass({
+ displayName: "PropRep",
+
+ propTypes: {
+ // Property name.
+ name: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.object,
+ ]).isRequired,
+ // Equal character rendered between property name and value.
+ equal: React.PropTypes.string,
+ // Delimiter character used to separate individual properties.
+ delim: React.PropTypes.string,
+ mode: React.PropTypes.string,
+ },
+
+ render: function () {
+ const { Grip } = require("./grip");
+ let { Rep } = createFactories(require("./rep"));
+
+ let key;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof this.props.name === "string") {
+ key = span({"className": "nodeName"}, this.props.name);
+ } else {
+ key = Rep({
+ object: this.props.name,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ objectLink: this.props.objectLink,
+ });
+ }
+
+ return (
+ span({},
+ key,
+ span({
+ "className": "objectEqual"
+ }, this.props.equal),
+ Rep(this.props),
+ span({
+ "className": "objectComma"
+ }, this.props.delim)
+ )
+ );
+ }
+ }));
+
+ // Exports from this module
+ exports.PropRep = PropRep;
+});
diff --git a/devtools/client/shared/components/reps/regexp.js b/devtools/client/shared/components/reps/regexp.js
new file mode 100644
index 000000000..2f9212658
--- /dev/null
+++ b/devtools/client/shared/components/reps/regexp.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a grip object with regular expression.
+ */
+ let RegExp = React.createClass({
+ displayName: "regexp",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getSource: function (grip) {
+ return grip.displayString;
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let objectLink = this.props.objectLink || span;
+
+ return (
+ span({className: "objectBox objectBox-regexp"},
+ objectLink({
+ object: grip,
+ className: "regexpSource"
+ }, this.getSource(grip))
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (type == "RegExp");
+ }
+
+ // Exports from this module
+ exports.RegExp = {
+ rep: RegExp,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/rep-utils.js b/devtools/client/shared/components/reps/rep-utils.js
new file mode 100644
index 000000000..d9580ac8d
--- /dev/null
+++ b/devtools/client/shared/components/reps/rep-utils.js
@@ -0,0 +1,160 @@
+/* globals URLSearchParams */
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ /**
+ * Create React factories for given arguments.
+ * Example:
+ * const { Rep } = createFactories(require("./rep"));
+ */
+ function createFactories(args) {
+ let result = {};
+ for (let p in args) {
+ result[p] = React.createFactory(args[p]);
+ }
+ return result;
+ }
+
+ /**
+ * Returns true if the given object is a grip (see RDP protocol)
+ */
+ function isGrip(object) {
+ return object && object.actor;
+ }
+
+ function escapeNewLines(value) {
+ return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+ }
+
+ function cropMultipleLines(text, limit) {
+ return escapeNewLines(cropString(text, limit));
+ }
+
+ function cropString(text, limit, alternativeText) {
+ if (!alternativeText) {
+ alternativeText = "\u2026";
+ }
+
+ // Make sure it's a string and sanitize it.
+ text = sanitizeString(text + "");
+
+ // Crop the string only if a limit is actually specified.
+ if (!limit || limit <= 0) {
+ return text;
+ }
+
+ // Set the limit at least to the length of the alternative text
+ // plus one character of the original text.
+ if (limit <= alternativeText.length) {
+ limit = alternativeText.length + 1;
+ }
+
+ let halfLimit = (limit - alternativeText.length) / 2;
+
+ if (text.length > limit) {
+ return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
+ text.substr(text.length - Math.floor(halfLimit));
+ }
+
+ return text;
+ }
+
+ function sanitizeString(text) {
+ // Replace all non-printable characters, except of
+ // (horizontal) tab (HT: \x09) and newline (LF: \x0A, CR: \x0D),
+ // with unicode replacement character (u+fffd).
+ // eslint-disable-next-line no-control-regex
+ let re = new RegExp("[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]", "g");
+ return text.replace(re, "\ufffd");
+ }
+
+ function parseURLParams(url) {
+ url = new URL(url);
+ return parseURLEncodedText(url.searchParams);
+ }
+
+ function parseURLEncodedText(text) {
+ let params = [];
+
+ // In case the text is empty just return the empty parameters
+ if (text == "") {
+ return params;
+ }
+
+ let searchParams = new URLSearchParams(text);
+ let entries = [...searchParams.entries()];
+ return entries.map(entry => {
+ return {
+ name: entry[0],
+ value: entry[1]
+ };
+ });
+ }
+
+ function getFileName(url) {
+ let split = splitURLBase(url);
+ return split.name;
+ }
+
+ function splitURLBase(url) {
+ if (!isDataURL(url)) {
+ return splitURLTrue(url);
+ }
+ return {};
+ }
+
+ function getURLDisplayString(url) {
+ return cropString(url);
+ }
+
+ function isDataURL(url) {
+ return (url && url.substr(0, 5) == "data:");
+ }
+
+ function splitURLTrue(url) {
+ const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+ let m = reSplitFile.exec(url);
+
+ if (!m) {
+ return {
+ name: url,
+ path: url
+ };
+ } else if (m[4] == "" && m[5] == "") {
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[3],
+ name: m[3] != "/" ? m[3] : m[2]
+ };
+ }
+
+ return {
+ protocol: m[1],
+ domain: m[2],
+ path: m[2] + m[3],
+ name: m[4] + m[5]
+ };
+ }
+
+ // Exports from this module
+ exports.createFactories = createFactories;
+ exports.isGrip = isGrip;
+ exports.cropString = cropString;
+ exports.cropMultipleLines = cropMultipleLines;
+ exports.parseURLParams = parseURLParams;
+ exports.parseURLEncodedText = parseURLEncodedText;
+ exports.getFileName = getFileName;
+ exports.getURLDisplayString = getURLDisplayString;
+ exports.sanitizeString = sanitizeString;
+});
diff --git a/devtools/client/shared/components/reps/rep.js b/devtools/client/shared/components/reps/rep.js
new file mode 100644
index 000000000..0891fe0ce
--- /dev/null
+++ b/devtools/client/shared/components/reps/rep.js
@@ -0,0 +1,144 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ const { isGrip } = require("./rep-utils");
+
+ // Load all existing rep templates
+ const { Undefined } = require("./undefined");
+ const { Null } = require("./null");
+ const { StringRep } = require("./string");
+ const { LongStringRep } = require("./long-string");
+ const { Number } = require("./number");
+ const { ArrayRep } = require("./array");
+ const { Obj } = require("./object");
+ const { SymbolRep } = require("./symbol");
+ const { InfinityRep } = require("./infinity");
+ const { NaNRep } = require("./nan");
+
+ // DOM types (grips)
+ const { Attribute } = require("./attribute");
+ const { DateTime } = require("./date-time");
+ const { Document } = require("./document");
+ const { Event } = require("./event");
+ const { Func } = require("./function");
+ const { PromiseRep } = require("./promise");
+ const { RegExp } = require("./regexp");
+ const { StyleSheet } = require("./stylesheet");
+ const { CommentNode } = require("./comment-node");
+ const { ElementNode } = require("./element-node");
+ const { TextNode } = require("./text-node");
+ const { Window } = require("./window");
+ const { ObjectWithText } = require("./object-with-text");
+ const { ObjectWithURL } = require("./object-with-url");
+ const { GripArray } = require("./grip-array");
+ const { GripMap } = require("./grip-map");
+ const { Grip } = require("./grip");
+
+ // List of all registered template.
+ // XXX there should be a way for extensions to register a new
+ // or modify an existing rep.
+ let reps = [
+ RegExp,
+ StyleSheet,
+ Event,
+ DateTime,
+ CommentNode,
+ ElementNode,
+ TextNode,
+ Attribute,
+ LongStringRep,
+ Func,
+ PromiseRep,
+ ArrayRep,
+ Document,
+ Window,
+ ObjectWithText,
+ ObjectWithURL,
+ GripArray,
+ GripMap,
+ Grip,
+ Undefined,
+ Null,
+ StringRep,
+ Number,
+ SymbolRep,
+ InfinityRep,
+ NaNRep,
+ ];
+
+ /**
+ * Generic rep that is using for rendering native JS types or an object.
+ * The right template used for rendering is picked automatically according
+ * to the current value type. The value must be passed is as 'object'
+ * property.
+ */
+ const Rep = React.createClass({
+ displayName: "Rep",
+
+ propTypes: {
+ object: React.PropTypes.any,
+ defaultRep: React.PropTypes.object,
+ mode: React.PropTypes.string
+ },
+
+ render: function () {
+ let rep = getRep(this.props.object, this.props.defaultRep);
+ return rep(this.props);
+ },
+ });
+
+ // Helpers
+
+ /**
+ * Return a rep object that is responsible for rendering given
+ * object.
+ *
+ * @param object {Object} Object to be rendered in the UI. This
+ * can be generic JS object as well as a grip (handle to a remote
+ * debuggee object).
+ *
+ * @param defaultObject {React.Component} The default template
+ * that should be used to render given object if none is found.
+ */
+ function getRep(object, defaultRep = Obj) {
+ let type = typeof object;
+ if (type == "object" && object instanceof String) {
+ type = "string";
+ } else if (object && type == "object" && object.type) {
+ type = object.type;
+ }
+
+ if (isGrip(object)) {
+ type = object.class;
+ }
+
+ for (let i = 0; i < reps.length; i++) {
+ let rep = reps[i];
+ try {
+ // supportsObject could return weight (not only true/false
+ // but a number), which would allow to priorities templates and
+ // support better extensibility.
+ if (rep.supportsObject(object, type)) {
+ return React.createFactory(rep.rep);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return React.createFactory(defaultRep.rep);
+ }
+
+ // Exports from this module
+ exports.Rep = Rep;
+});
diff --git a/devtools/client/shared/components/reps/reps.css b/devtools/client/shared/components/reps/reps.css
new file mode 100644
index 000000000..61e5e3dac
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps.css
@@ -0,0 +1,174 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.theme-dark,
+.theme-light {
+ --number-color: var(--theme-highlight-green);
+ --string-color: var(--theme-highlight-orange);
+ --null-color: var(--theme-comment);
+ --object-color: var(--theme-body-color);
+ --caption-color: var(--theme-highlight-blue);
+ --location-color: var(--theme-content-color1);
+ --source-link-color: var(--theme-highlight-blue);
+ --node-color: var(--theme-highlight-bluegrey);
+ --reference-color: var(--theme-highlight-purple);
+}
+
+.theme-firebug {
+ --number-color: #000088;
+ --string-color: #FF0000;
+ --null-color: #787878;
+ --object-color: DarkGreen;
+ --caption-color: #444444;
+ --location-color: #555555;
+ --source-link-color: blue;
+ --node-color: rgb(0, 0, 136);
+ --reference-color: rgb(102, 102, 255);
+}
+
+/******************************************************************************/
+
+.objectLink:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.inline {
+ display: inline;
+ white-space: normal;
+}
+
+.objectBox-object {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-symbol,
+.objectBox-text,
+.objectLink-textNode,
+.objectBox-table {
+ white-space: pre-wrap;
+}
+
+.objectBox-number,
+.objectLink-styleRule,
+.objectLink-element,
+.objectLink-textNode,
+.objectBox-array > .length {
+ color: var(--number-color);
+}
+
+.objectBox-textNode,
+.objectBox-string,
+.objectBox-symbol {
+ color: var(--string-color);
+}
+
+.objectLink-function,
+.objectBox-stackTrace,
+.objectLink-profile {
+ color: var(--object-color);
+}
+
+.objectLink-Location {
+ font-style: italic;
+ color: var(--location-color);
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.logRowHint {
+ font-style: italic;
+ color: var(--null-color);
+}
+
+.objectLink-sourceLink {
+ position: absolute;
+ right: 4px;
+ top: 2px;
+ padding-left: 8px;
+ font-weight: bold;
+ color: var(--source-link-color);
+}
+
+/******************************************************************************/
+
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date {
+ font-weight: bold;
+ color: var(--object-color);
+ white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectLink-object .nodeName,
+.objectLink-NamedNodeMap .nodeName,
+.objectLink-NamedNodeMap .objectEqual,
+.objectLink-NamedNodeMap .arrayLeftBracket,
+.objectLink-NamedNodeMap .arrayRightBracket,
+.objectLink-Attr .attrEqual,
+.objectLink-Attr .attrTitle {
+ color: var(--node-color);
+}
+
+.objectLink-object .nodeName {
+ font-weight: normal;
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+ cursor: pointer;
+ font-weight: bold;
+}
+
+/******************************************************************************/
+/* Cycle reference*/
+
+.objectLink-Reference {
+ font-weight: bold;
+ color: var(--reference-color);
+}
+
+.objectBox-array > .objectTitle {
+ font-weight: bold;
+ color: var(--object-color);
+}
+
+.caption {
+ font-weight: bold;
+ color: var(--caption-color);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+ font-style: normal;
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ font-weight: normal;
+ white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+ font-weight: normal;
+}
diff --git a/devtools/client/shared/components/reps/string.js b/devtools/client/shared/components/reps/string.js
new file mode 100644
index 000000000..f8b4b1986
--- /dev/null
+++ b/devtools/client/shared/components/reps/string.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a string. String value is enclosed within quotes.
+ */
+ const StringRep = React.createClass({
+ displayName: "StringRep",
+
+ propTypes: {
+ useQuotes: React.PropTypes.bool,
+ style: React.PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ useQuotes: true,
+ };
+ },
+
+ render: function () {
+ let text = this.props.object;
+ let member = this.props.member;
+ let style = this.props.style;
+
+ let config = {className: "objectBox objectBox-string"};
+ if (style) {
+ config.style = style;
+ }
+
+ if (member && member.open) {
+ return span(config, "\"" + text + "\"");
+ }
+
+ let croppedString = this.props.cropLimit ?
+ cropString(text, this.props.cropLimit) : cropString(text);
+
+ let formattedString = this.props.useQuotes ?
+ "\"" + croppedString + "\"" : croppedString;
+
+ return span(config, formattedString);
+ },
+ });
+
+ function supportsObject(object, type) {
+ return (type == "string");
+ }
+
+ // Exports from this module
+
+ exports.StringRep = {
+ rep: StringRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/stylesheet.js b/devtools/client/shared/components/reps/stylesheet.js
new file mode 100644
index 000000000..c1fc7f1be
--- /dev/null
+++ b/devtools/client/shared/components/reps/stylesheet.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders a grip representing CSSStyleSheet
+ */
+ let StyleSheet = React.createClass({
+ displayName: "object",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ let title = "StyleSheet ";
+ if (this.props.objectLink) {
+ return DOM.span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, title)
+ );
+ }
+ return title;
+ },
+
+ getLocation: function (grip) {
+ // Embedded stylesheets don't have URL and so, no preview.
+ let url = grip.preview ? grip.preview.url : "";
+ return url ? getURLDisplayString(url) : "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ DOM.span({className: "objectBox objectBox-object"},
+ this.getTitle(grip),
+ DOM.span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (type == "CSSStyleSheet");
+ }
+
+ // Exports from this module
+
+ exports.StyleSheet = {
+ rep: StyleSheet,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/symbol.js b/devtools/client/shared/components/reps/symbol.js
new file mode 100644
index 000000000..111794008
--- /dev/null
+++ b/devtools/client/shared/components/reps/symbol.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders a symbol.
+ */
+ const SymbolRep = React.createClass({
+ displayName: "SymbolRep",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired
+ },
+
+ render: function () {
+ let {object} = this.props;
+ let {name} = object;
+
+ return (
+ span({className: "objectBox objectBox-symbol"},
+ `Symbol(${name || ""})`
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ return (type == "symbol");
+ }
+
+ // Exports from this module
+ exports.SymbolRep = {
+ rep: SymbolRep,
+ supportsObject: supportsObject,
+ };
+});
diff --git a/devtools/client/shared/components/reps/text-node.js b/devtools/client/shared/components/reps/text-node.js
new file mode 100644
index 000000000..d80545cea
--- /dev/null
+++ b/devtools/client/shared/components/reps/text-node.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, cropString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders DOM #text node.
+ */
+ let TextNode = React.createClass({
+ displayName: "TextNode",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ mode: React.PropTypes.string,
+ },
+
+ getTextContent: function (grip) {
+ return cropString(grip.preview.textContent);
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: grip
+ }, "#text ");
+ }
+ return "";
+ },
+
+ render: function () {
+ let grip = this.props.object;
+ let mode = this.props.mode || "short";
+
+ if (mode == "short" || mode == "tiny") {
+ return (
+ DOM.span({className: "objectBox objectBox-textNode"},
+ this.getTitle(grip),
+ DOM.span({className: "nodeValue"},
+ "\"" + this.getTextContent(grip) + "\""
+ )
+ )
+ );
+ }
+
+ let objectLink = this.props.objectLink || DOM.span;
+ return (
+ DOM.span({className: "objectBox objectBox-textNode"},
+ this.getTitle(grip),
+ objectLink({
+ object: grip
+ }, "<"),
+ DOM.span({className: "nodeTag"}, "TextNode"),
+ " textContent=\"",
+ DOM.span({className: "nodeValue"},
+ this.getTextContent(grip)
+ ),
+ "\"",
+ objectLink({
+ object: grip
+ }, ">;")
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+
+ return (grip.preview && grip.class == "Text");
+ }
+
+ // Exports from this module
+ exports.TextNode = {
+ rep: TextNode,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/undefined.js b/devtools/client/shared/components/reps/undefined.js
new file mode 100644
index 000000000..c4e64a12c
--- /dev/null
+++ b/devtools/client/shared/components/reps/undefined.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { span } = React.DOM;
+
+ /**
+ * Renders undefined value
+ */
+ const Undefined = React.createClass({
+ displayName: "UndefinedRep",
+
+ render: function () {
+ return (
+ span({className: "objectBox objectBox-undefined"},
+ "undefined"
+ )
+ );
+ },
+ });
+
+ function supportsObject(object, type) {
+ if (object && object.type && object.type == "undefined") {
+ return true;
+ }
+
+ return (type == "undefined");
+ }
+
+ // Exports from this module
+
+ exports.Undefined = {
+ rep: Undefined,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/reps/window.js b/devtools/client/shared/components/reps/window.js
new file mode 100644
index 000000000..628d69562
--- /dev/null
+++ b/devtools/client/shared/components/reps/window.js
@@ -0,0 +1,73 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { isGrip, getURLDisplayString } = require("./rep-utils");
+
+ // Shortcuts
+ const DOM = React.DOM;
+
+ /**
+ * Renders a grip representing a window.
+ */
+ let Window = React.createClass({
+ displayName: "Window",
+
+ propTypes: {
+ object: React.PropTypes.object.isRequired,
+ },
+
+ getTitle: function (grip) {
+ if (this.props.objectLink) {
+ return DOM.span({className: "objectBox"},
+ this.props.objectLink({
+ object: grip
+ }, grip.class + " ")
+ );
+ }
+ return "";
+ },
+
+ getLocation: function (grip) {
+ return getURLDisplayString(grip.preview.url);
+ },
+
+ render: function () {
+ let grip = this.props.object;
+
+ return (
+ DOM.span({className: "objectBox objectBox-Window"},
+ this.getTitle(grip),
+ DOM.span({className: "objectPropValue"},
+ this.getLocation(grip)
+ )
+ )
+ );
+ },
+ });
+
+ // Registration
+
+ function supportsObject(object, type) {
+ if (!isGrip(object)) {
+ return false;
+ }
+
+ return (object.preview && type == "Window");
+ }
+
+ // Exports from this module
+ exports.Window = {
+ rep: Window,
+ supportsObject: supportsObject
+ };
+});
diff --git a/devtools/client/shared/components/search-box.js b/devtools/client/shared/components/search-box.js
new file mode 100644
index 000000000..bd572f8b2
--- /dev/null
+++ b/devtools/client/shared/components/search-box.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/* global window */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+/**
+ * A generic search box component for use across devtools
+ */
+module.exports = createClass({
+ displayName: "SearchBox",
+
+ propTypes: {
+ delay: PropTypes.number,
+ keyShortcut: PropTypes.string,
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ type: PropTypes.string
+ },
+
+ getInitialState() {
+ return {
+ value: ""
+ };
+ },
+
+ componentDidMount() {
+ if (!this.props.keyShortcut) {
+ return;
+ }
+
+ this.shortcuts = new KeyShortcuts({
+ window
+ });
+ this.shortcuts.on(this.props.keyShortcut, (name, event) => {
+ event.preventDefault();
+ this.refs.input.focus();
+ });
+ },
+
+ componentWillUnmount() {
+ if (this.shortcuts) {
+ this.shortcuts.destroy();
+ }
+
+ // Clean up an existing timeout.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+ },
+
+ onChange() {
+ if (this.state.value !== this.refs.input.value) {
+ this.setState({ value: this.refs.input.value });
+ }
+
+ if (!this.props.delay) {
+ this.props.onChange(this.state.value);
+ return;
+ }
+
+ // Clean up an existing timeout before creating a new one.
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+
+ // Execute the search after a timeout. It makes the UX
+ // smoother if the user is typing quickly.
+ this.searchTimeout = setTimeout(() => {
+ this.searchTimeout = null;
+ this.props.onChange(this.state.value);
+ }, this.props.delay);
+ },
+
+ onClearButtonClick() {
+ this.refs.input.value = "";
+ this.onChange();
+ },
+
+ render() {
+ let { type = "search", placeholder } = this.props;
+ let { value } = this.state;
+ let divClassList = ["devtools-searchbox", "has-clear-btn"];
+ let inputClassList = [`devtools-${type}input`];
+
+ if (value !== "") {
+ inputClassList.push("filled");
+ }
+ return dom.div(
+ { className: divClassList.join(" ") },
+ dom.input({
+ className: inputClassList.join(" "),
+ onChange: this.onChange,
+ placeholder,
+ ref: "input",
+ value
+ }),
+ dom.button({
+ className: "devtools-searchinput-clear",
+ hidden: value == "",
+ onClick: this.onClearButtonClick
+ })
+ );
+ }
+});
diff --git a/devtools/client/shared/components/sidebar-toggle.css b/devtools/client/shared/components/sidebar-toggle.css
new file mode 100644
index 000000000..659a3d23f
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.css
@@ -0,0 +1,32 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.sidebar-toggle {
+ display: block;
+}
+
+.sidebar-toggle::before,
+.sidebar-toggle.pane-collapsed:dir(rtl)::before {
+ background-image: var(--theme-pane-collapse-image);
+}
+
+.sidebar-toggle.pane-collapsed::before,
+.sidebar-toggle:dir(rtl)::before {
+ background-image: var(--theme-pane-expand-image);
+}
+
+/* Rotate button icon 90deg if the toolbox container is
+ in vertical mode (sidebar displayed under the main panel) */
+@media (max-width: 700px) {
+ .sidebar-toggle::before {
+ transform: rotate(90deg);
+ }
+
+ /* Since RTL swaps the used images, we need to flip them
+ the other way round */
+ .sidebar-toggle:dir(rtl)::before {
+ transform: rotate(-90deg);
+ }
+}
diff --git a/devtools/client/shared/components/sidebar-toggle.js b/devtools/client/shared/components/sidebar-toggle.js
new file mode 100644
index 000000000..013e95f3d
--- /dev/null
+++ b/devtools/client/shared/components/sidebar-toggle.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const { button } = DOM;
+
+/**
+ * Sidebar toggle button. This button is used to exapand
+ * and collapse Sidebar.
+ */
+var SidebarToggle = createClass({
+ displayName: "SidebarToggle",
+
+ propTypes: {
+ // Set to true if collapsed.
+ collapsed: PropTypes.bool.isRequired,
+ // Tooltip text used when the button indicates expanded state.
+ collapsePaneTitle: PropTypes.string.isRequired,
+ // Tooltip text used when the button indicates collapsed state.
+ expandPaneTitle: PropTypes.string.isRequired,
+ // Click callback
+ onClick: PropTypes.func.isRequired,
+ },
+
+ getInitialState: function () {
+ return {
+ collapsed: this.props.collapsed,
+ };
+ },
+
+ // Events
+
+ onClick: function (event) {
+ this.props.onClick(event);
+ },
+
+ // Rendering
+
+ render: function () {
+ let title = this.state.collapsed ?
+ this.props.expandPaneTitle :
+ this.props.collapsePaneTitle;
+
+ let classNames = ["devtools-button", "sidebar-toggle"];
+ if (this.state.collapsed) {
+ classNames.push("pane-collapsed");
+ }
+
+ return (
+ button({
+ className: classNames.join(" "),
+ title: title,
+ onClick: this.onClick
+ })
+ );
+ }
+});
+
+module.exports = SidebarToggle;
diff --git a/devtools/client/shared/components/splitter/draggable.js b/devtools/client/shared/components/splitter/draggable.js
new file mode 100644
index 000000000..9caf93089
--- /dev/null
+++ b/devtools/client/shared/components/splitter/draggable.js
@@ -0,0 +1,54 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { DOM: dom, PropTypes } = React;
+
+const Draggable = React.createClass({
+ displayName: "Draggable",
+
+ propTypes: {
+ onMove: PropTypes.func.isRequired,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ className: PropTypes.string
+ },
+
+ startDragging(ev) {
+ ev.preventDefault();
+ const doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.addEventListener("mousemove", this.onMove);
+ doc.addEventListener("mouseup", this.onUp);
+ this.props.onStart && this.props.onStart();
+ },
+
+ onMove(ev) {
+ ev.preventDefault();
+ // Use viewport coordinates so, moving mouse over iframes
+ // doesn't mangle (relative) coordinates.
+ this.props.onMove(ev.clientX, ev.clientY);
+ },
+
+ onUp(ev) {
+ ev.preventDefault();
+ const doc = ReactDOM.findDOMNode(this).ownerDocument;
+ doc.removeEventListener("mousemove", this.onMove);
+ doc.removeEventListener("mouseup", this.onUp);
+ this.props.onStop && this.props.onStop();
+ },
+
+ render() {
+ return dom.div({
+ style: this.props.style,
+ className: this.props.className,
+ onMouseDown: this.startDragging
+ });
+ }
+});
+
+module.exports = Draggable;
diff --git a/devtools/client/shared/components/splitter/moz.build b/devtools/client/shared/components/splitter/moz.build
new file mode 100644
index 000000000..924732aea
--- /dev/null
+++ b/devtools/client/shared/components/splitter/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'draggable.js',
+ 'split-box.css',
+ 'split-box.js',
+)
diff --git a/devtools/client/shared/components/splitter/split-box.css b/devtools/client/shared/components/splitter/split-box.css
new file mode 100644
index 000000000..ea8fdaa6f
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.css
@@ -0,0 +1,88 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.split-box {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.split-box.vert {
+ flex-direction: row;
+}
+
+.split-box.horz {
+ flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+}
+
+.split-box > .controlled {
+ display: flex;
+ overflow: auto;
+}
+
+.split-box > .splitter {
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--theme-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.split-box.vert > .splitter {
+ min-width: calc(var(--devtools-splitter-inline-start-width) +
+ var(--devtools-splitter-inline-end-width) + 1px);
+
+ border-inline-start-width: var(--devtools-splitter-inline-start-width);
+ border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+ margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+ min-height: calc(var(--devtools-splitter-top-width) +
+ var(--devtools-splitter-bottom-width) + 1px);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: ns-resize;
+}
+
+.split-box.disabled {
+ pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+ pointer-events: none;
+}
diff --git a/devtools/client/shared/components/splitter/split-box.js b/devtools/client/shared/components/splitter/split-box.js
new file mode 100644
index 000000000..85835f3e1
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.js
@@ -0,0 +1,205 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/draggable"));
+const { DOM: dom, PropTypes } = React;
+
+/**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+const SplitBox = React.createClass({
+ displayName: "SplitBox",
+
+ propTypes: {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.number,
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Min panel size.
+ minSize: PropTypes.number,
+ // Max panel size.
+ maxSize: PropTypes.number,
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.number,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool
+ },
+
+ getDefaultProps() {
+ return {
+ splitterSize: 5,
+ vert: true,
+ endPanelControl: false
+ };
+ },
+
+ /**
+ * The state stores the current orientation (vertical or horizontal)
+ * and the current size (width/height). All these values can change
+ * during the component's life time.
+ */
+ getInitialState() {
+ return {
+ vert: this.props.vert,
+ width: this.props.initialWidth || this.props.initialSize,
+ height: this.props.initialHeight || this.props.initialSize
+ };
+ },
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const splitBox = ReactDOM.findDOMNode(this);
+ const doc = splitBox.ownerDocument;
+ let defaultCursor = doc.documentElement.style.cursor;
+ doc.documentElement.style.cursor = (this.state.vert ? "ew-resize" : "ns-resize");
+
+ splitBox.classList.add("dragging");
+
+ this.setState({
+ defaultCursor: defaultCursor
+ });
+ },
+
+ onStopMove() {
+ const splitBox = ReactDOM.findDOMNode(this);
+ const doc = splitBox.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+
+ splitBox.classList.remove("dragging");
+ },
+
+ /**
+ * Adjust size of the controlled panel. Depending on the current
+ * orientation we either remember the width or height of
+ * the splitter box.
+ */
+ onMove(x, y) {
+ const node = ReactDOM.findDOMNode(this);
+ const doc = node.ownerDocument;
+ const win = doc.defaultView;
+
+ let size;
+ let { endPanelControl } = this.props;
+
+ if (this.state.vert) {
+ // Switch the control flag in case of RTL. Note that RTL
+ // has impact on vertical splitter only.
+ let dir = win.getComputedStyle(doc.documentElement).direction;
+ if (dir == "rtl") {
+ endPanelControl = !endPanelControl;
+ }
+
+ size = endPanelControl ?
+ (node.offsetLeft + node.offsetWidth) - x :
+ x - node.offsetLeft;
+
+ this.setState({
+ width: size
+ });
+ } else {
+ size = endPanelControl ?
+ (node.offsetTop + node.offsetHeight) - y :
+ y - node.offsetTop;
+
+ this.setState({
+ height: size
+ });
+ }
+ },
+
+ // Rendering
+
+ render() {
+ const vert = this.state.vert;
+ const { startPanel, endPanel, endPanelControl, minSize,
+ maxSize, splitterSize } = this.props;
+
+ let style = Object.assign({}, this.props.style);
+
+ // Calculate class names list.
+ let classNames = ["split-box"];
+ classNames.push(vert ? "vert" : "horz");
+ if (this.props.className) {
+ classNames = classNames.concat(this.props.className.split(" "));
+ }
+
+ let leftPanelStyle;
+ let rightPanelStyle;
+
+ // Set proper size for panels depending on the current state.
+ if (vert) {
+ leftPanelStyle = {
+ maxWidth: endPanelControl ? null : maxSize,
+ minWidth: endPanelControl ? null : minSize,
+ width: endPanelControl ? null : this.state.width
+ };
+ rightPanelStyle = {
+ maxWidth: endPanelControl ? maxSize : null,
+ minWidth: endPanelControl ? minSize : null,
+ width: endPanelControl ? this.state.width : null
+ };
+ } else {
+ leftPanelStyle = {
+ maxHeight: endPanelControl ? null : maxSize,
+ minHeight: endPanelControl ? null : minSize,
+ height: endPanelControl ? null : this.state.height
+ };
+ rightPanelStyle = {
+ maxHeight: endPanelControl ? maxSize : null,
+ minHeight: endPanelControl ? minSize : null,
+ height: endPanelControl ? this.state.height : null
+ };
+ }
+
+ // Calculate splitter size
+ let splitterStyle = {
+ flex: "0 0 " + splitterSize + "px"
+ };
+
+ return (
+ dom.div({
+ className: classNames.join(" "),
+ style: style },
+ startPanel ?
+ dom.div({
+ className: endPanelControl ? "uncontrolled" : "controlled",
+ style: leftPanelStyle},
+ startPanel
+ ) : null,
+ Draggable({
+ className: "splitter",
+ style: splitterStyle,
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove
+ }),
+ endPanel ?
+ dom.div({
+ className: endPanelControl ? "controlled" : "uncontrolled",
+ style: rightPanelStyle},
+ endPanel
+ ) : null
+ )
+ );
+ }
+});
+
+module.exports = SplitBox;
diff --git a/devtools/client/shared/components/stack-trace.js b/devtools/client/shared/components/stack-trace.js
new file mode 100644
index 000000000..43d0b8716
--- /dev/null
+++ b/devtools/client/shared/components/stack-trace.js
@@ -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/. */
+
+"use strict";
+
+const React = require("devtools/client/shared/vendor/react");
+const { DOM: dom, createClass, createFactory, PropTypes } = React;
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const Frame = createFactory(require("./frame"));
+
+const l10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
+
+const AsyncFrame = createFactory(createClass({
+ displayName: "AsyncFrame",
+
+ PropTypes: {
+ asyncCause: PropTypes.string.isRequired
+ },
+
+ render() {
+ let { asyncCause } = this.props;
+
+ return dom.span(
+ { className: "frame-link-async-cause" },
+ l10n.getFormatStr("stacktrace.asyncStack", asyncCause)
+ );
+ }
+}));
+
+const StackTrace = createClass({
+ displayName: "StackTrace",
+
+ PropTypes: {
+ stacktrace: PropTypes.array.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired
+ },
+
+ render() {
+ let { stacktrace, onViewSourceInDebugger } = this.props;
+
+ let frames = [];
+ stacktrace.forEach(s => {
+ if (s.asyncCause) {
+ frames.push("\t", AsyncFrame({
+ asyncCause: s.asyncCause
+ }), "\n");
+ }
+
+ frames.push("\t", Frame({
+ frame: {
+ functionDisplayName: s.functionName,
+ source: s.filename.split(" -> ").pop(),
+ line: s.lineNumber,
+ column: s.columnNumber,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true,
+ showFullSourceUrl: true,
+ onClick: onViewSourceInDebugger
+ }), "\n");
+ });
+
+ return dom.div({ className: "stack-trace" }, frames);
+ }
+});
+
+module.exports = StackTrace;
diff --git a/devtools/client/shared/components/tabs/moz.build b/devtools/client/shared/components/tabs/moz.build
new file mode 100644
index 000000000..d4d5dc35d
--- /dev/null
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'tabbar.css',
+ 'tabbar.js',
+ 'tabs.css',
+ 'tabs.js',
+)
diff --git a/devtools/client/shared/components/tabs/tabbar.css b/devtools/client/shared/components/tabs/tabbar.css
new file mode 100644
index 000000000..72445e43e
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -0,0 +1,53 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.tabs .tabs-navigation {
+ line-height: 15px;
+}
+
+.tabs .tabs-navigation {
+ height: 24px;
+}
+
+.tabs .tabs-menu-item:first-child {
+ border-inline-start-width: 0;
+}
+
+.tabs .tabs-navigation .tabs-menu-item:focus {
+ outline: var(--theme-focus-outline);
+ outline-offset: -2px;
+}
+
+.tabs .tabs-menu-item.is-active {
+ height: 23px;
+}
+
+/* Firebug theme is using slightly different height. */
+.theme-firebug .tabs .tabs-navigation {
+ height: 24px;
+}
+
+/* The tab takes entire horizontal space and individual tabs
+ should stretch accordingly. Use flexbox for the behavior.
+ Use also `overflow: hidden` so, 'overflow' and 'underflow'
+ events are fired (it's utilized by the all-tabs-menu). */
+.tabs .tabs-navigation .tabs-menu {
+ overflow: hidden;
+ display: flex;
+}
+
+.tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 1;
+}
+
+.tabs .tabs-navigation .tabs-menu-item a {
+ text-align: center;
+}
+
+/* Firebug theme doesn't stretch the tabs. */
+.theme-firebug .tabs .tabs-navigation .tabs-menu-item {
+ flex-grow: 0;
+}
+
diff --git a/devtools/client/shared/components/tabs/tabbar.js b/devtools/client/shared/components/tabs/tabbar.js
new file mode 100644
index 000000000..1e3aa4617
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -0,0 +1,204 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
+
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+// Shortcuts
+const { div } = DOM;
+
+/**
+ * Renders Tabbar component.
+ */
+let Tabbar = createClass({
+ displayName: "Tabbar",
+
+ propTypes: {
+ onSelect: PropTypes.func,
+ showAllTabsMenu: PropTypes.bool,
+ toolbox: PropTypes.object,
+ },
+
+ getDefaultProps: function () {
+ return {
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabs: [],
+ activeTab: 0
+ };
+ },
+
+ // Public API
+
+ addTab: function (id, title, selected = false, panel, url) {
+ let tabs = this.state.tabs.slice();
+ tabs.push({id, title, panel, url});
+
+ let newState = Object.assign({}, this.state, {
+ tabs: tabs,
+ });
+
+ if (selected) {
+ newState.activeTab = tabs.length - 1;
+ }
+
+ this.setState(newState, () => {
+ if (this.props.onSelect && selected) {
+ this.props.onSelect(id);
+ }
+ });
+ },
+
+ toggleTab: function (tabId, isVisible) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs[index] = Object.assign({}, tabs[index], {
+ isVisible: isVisible
+ });
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ removeTab: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let tabs = this.state.tabs.slice();
+ tabs.splice(index, 1);
+
+ this.setState(Object.assign({}, this.state, {
+ tabs: tabs,
+ }));
+ },
+
+ select: function (tabId) {
+ let index = this.getTabIndex(tabId);
+ if (index < 0) {
+ return;
+ }
+
+ let newState = Object.assign({}, this.state, {
+ activeTab: index,
+ });
+
+ this.setState(newState, () => {
+ if (this.props.onSelect) {
+ this.props.onSelect(tabId);
+ }
+ });
+ },
+
+ // Helpers
+
+ getTabIndex: function (tabId) {
+ let tabIndex = -1;
+ this.state.tabs.forEach((tab, index) => {
+ if (tab.id == tabId) {
+ tabIndex = index;
+ }
+ });
+ return tabIndex;
+ },
+
+ getTabId: function (index) {
+ return this.state.tabs[index].id;
+ },
+
+ getCurrentTabId: function () {
+ return this.state.tabs[this.state.activeTab].id;
+ },
+
+ // Event Handlers
+
+ onTabChanged: function (index) {
+ this.setState({
+ activeTab: index
+ });
+
+ if (this.props.onSelect) {
+ this.props.onSelect(this.state.tabs[index].id);
+ }
+ },
+
+ onAllTabsMenuClick: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of tabs.
+ this.state.tabs.forEach(tab => {
+ menu.append(new MenuItem({
+ label: tab.title,
+ type: "checkbox",
+ checked: this.getCurrentTabId() == tab.id,
+ click: () => this.select(tab.id),
+ }));
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this.props.toolbox);
+
+ return menu;
+ },
+
+ // Rendering
+
+ renderTab: function (tab) {
+ if (typeof tab.panel === "function") {
+ return tab.panel({
+ key: tab.id,
+ title: tab.title,
+ id: tab.id,
+ url: tab.url,
+ });
+ }
+
+ return tab.panel;
+ },
+
+ render: function () {
+ let tabs = this.state.tabs.map(tab => {
+ return this.renderTab(tab);
+ });
+
+ return (
+ div({className: "devtools-sidebar-tabs"},
+ Tabs({
+ onAllTabsMenuClick: this.onAllTabsMenuClick,
+ showAllTabsMenu: this.props.showAllTabsMenu,
+ tabActive: this.state.activeTab,
+ onAfterChange: this.onTabChanged},
+ tabs
+ )
+ )
+ );
+ },
+});
+
+module.exports = Tabbar;
diff --git a/devtools/client/shared/components/tabs/tabs.css b/devtools/client/shared/components/tabs/tabs.css
new file mode 100644
index 000000000..0e70549c5
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -0,0 +1,183 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/* Tabs General Styles */
+
+.tabs {
+ height: 100%;
+}
+
+.tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.tabs .tabs-menu-item {
+ display: inline-block;
+}
+
+.tabs .tabs-menu-item a {
+ display: block;
+ color: #A9A9A9;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.tabs .tabs-menu-item a {
+ cursor: default;
+}
+
+/* Make sure panel content takes entire vertical space.
+ (minus the height of the tab bar) */
+.tabs .panels {
+ height: calc(100% - 24px);
+}
+
+.tabs .tab-panel {
+ height: 100%;
+}
+
+.tabs .all-tabs-menu {
+ position: absolute;
+ top: 0;
+ offset-inline-end: 0;
+ width: 15px;
+ height: 100%;
+ border-inline-start: 1px solid var(--theme-splitter-color);
+ background: url("chrome://devtools/skin/images/dropmarker.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+/* Light Theme */
+
+.theme-dark .tabs,
+.theme-light .tabs {
+ background: var(--theme-body-background);
+}
+
+.theme-dark .tabs .tabs-navigation,
+.theme-light .tabs .tabs-navigation {
+ position: relative;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .tabs .tabs-menu-item,
+.theme-light .tabs .tabs-menu-item {
+ margin: 0;
+ padding: 0;
+ border-style: solid;
+ border-width: 0;
+ border-inline-start-width: 1px;
+ border-color: var(--theme-splitter-color);
+}
+
+.theme-dark .tabs .tabs-menu-item:last-child,
+.theme-light:not(.theme-firebug) .tabs .tabs-menu-item:last-child {
+ border-inline-end-width: 1px;
+}
+
+.theme-dark .tabs .tabs-menu-item a,
+.theme-light .tabs .tabs-menu-item a {
+ color: var(--theme-content-color1);
+ padding: 3px 15px;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:not(.is-active) {
+ background-color: var(--toolbar-tab-hover);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active:not(.is-active),
+.theme-light .tabs .tabs-menu-item:hover:active:not(.is-active) {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active,
+.theme-light .tabs .tabs-menu-item.is-active {
+ background-color: var(--theme-selection-background);
+}
+
+.theme-dark .tabs .tabs-menu-item.is-active a,
+.theme-light .tabs .tabs-menu-item.is-active a {
+ color: var(--theme-selection-color);
+}
+
+/* Dark Theme */
+
+.theme-dark .tabs .tabs-menu-item a {
+ color: var(--theme-body-color-alt);
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:not(.is-active) a {
+ color: #CED3D9;
+}
+
+.theme-dark .tabs .tabs-menu-item:hover:active a {
+ color: var(--theme-selection-color);
+}
+
+/* Firebug Theme */
+
+.theme-firebug .tabs .tabs-navigation {
+ background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+ padding-top: 3px;
+ padding-left: 3px;
+ border-bottom: 1px solid rgb(170, 188, 207);
+}
+
+.theme-firebug .tabs .tabs-menu {
+ margin-bottom: -1px;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active,
+.theme-firebug .tabs .tabs-menu-item.is-active:hover {
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item {
+ position: relative;
+ border-inline-start-width: 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ font-family: var(--proportional-font-family);
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:not(.is-active) a {
+ border: 1px solid #C8C8C8;
+ border-bottom: 1px solid transparent;
+ background-color: transparent;
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active a {
+ background-color: rgb(247, 251, 254);
+ border: 1px solid rgb(170, 188, 207);
+ border-bottom-color: transparent;
+ color: var(--theme-body-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item:hover:active a {
+ background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-firebug .tabs .tabs-menu-item.is-active:hover:active a {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.theme-firebug .tabs .tabs-menu-item a {
+ border: 1px solid transparent;
+ padding: 4px 8px;
+}
diff --git a/devtools/client/shared/components/tabs/tabs.js b/devtools/client/shared/components/tabs/tabs.js
new file mode 100644
index 000000000..eaa0738b3
--- /dev/null
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -0,0 +1,369 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+ const { DOM } = React;
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+
+ /**
+ * Renders simple 'tab' widget.
+ *
+ * Based on ReactSimpleTabs component
+ * https://github.com/pedronauck/react-simpletabs
+ *
+ * Component markup (+CSS) example:
+ *
+ * <div class='tabs'>
+ * <nav class='tabs-navigation'>
+ * <ul class='tabs-menu'>
+ * <li class='tabs-menu-item is-active'>Tab #1</li>
+ * <li class='tabs-menu-item'>Tab #2</li>
+ * </ul>
+ * </nav>
+ * <div class='panels'>
+ * The content of active panel here
+ * </div>
+ * <div>
+ */
+ let Tabs = React.createClass({
+ displayName: "Tabs",
+
+ propTypes: {
+ className: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.string,
+ React.PropTypes.object
+ ]),
+ tabActive: React.PropTypes.number,
+ onMount: React.PropTypes.func,
+ onBeforeChange: React.PropTypes.func,
+ onAfterChange: React.PropTypes.func,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired,
+ showAllTabsMenu: React.PropTypes.bool,
+ onAllTabsMenuClick: React.PropTypes.func,
+ },
+
+ getDefaultProps: function () {
+ return {
+ tabActive: 0,
+ showAllTabsMenu: false,
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ tabActive: this.props.tabActive,
+
+ // This array is used to store an information whether a tab
+ // at specific index has already been created (e.g. selected
+ // at least once).
+ // If yes, it's rendered even if not currently selected.
+ // This is because in some cases we don't want to re-create
+ // tab content when it's being unselected/selected.
+ // E.g. in case of an iframe being used as a tab-content
+ // we want the iframe to stay in the DOM.
+ created: [],
+
+ // True if tabs can't fit into available horizontal space.
+ overflow: false,
+ };
+ },
+
+ componentDidMount: function () {
+ let node = findDOMNode(this);
+ node.addEventListener("keydown", this.onKeyDown, false);
+
+ // Register overflow listeners to manage visibility
+ // of all-tabs-menu. This menu is displayed when there
+ // is not enough h-space to render all tabs.
+ // It allows the user to select a tab even if it's hidden.
+ if (this.props.showAllTabsMenu) {
+ node.addEventListener("overflow", this.onOverflow, false);
+ node.addEventListener("underflow", this.onUnderflow, false);
+ }
+
+ let index = this.state.tabActive;
+ if (this.props.onMount) {
+ this.props.onMount(index);
+ }
+ },
+
+ componentWillReceiveProps: function (newProps) {
+ // Check type of 'tabActive' props to see if it's valid
+ // (it's 0-based index).
+ if (typeof newProps.tabActive == "number") {
+ let created = [...this.state.created];
+ created[newProps.tabActive] = true;
+
+ this.setState(Object.assign({}, this.state, {
+ tabActive: newProps.tabActive,
+ created: created,
+ }));
+ }
+ },
+
+ componentWillUnmount: function () {
+ let node = findDOMNode(this);
+ node.removeEventListener("keydown", this.onKeyDown, false);
+
+ if (this.props.showAllTabsMenu) {
+ node.removeEventListener("overflow", this.onOverflow, false);
+ node.removeEventListener("underflow", this.onUnderflow, false);
+ }
+ },
+
+ // DOM Events
+
+ onOverflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: true
+ });
+ }
+ },
+
+ onUnderflow: function (event) {
+ if (event.target.classList.contains("tabs-menu")) {
+ this.setState({
+ overflow: false
+ });
+ }
+ },
+
+ onKeyDown: function (event) {
+ // Bail out if the focus isn't on a tab.
+ if (!event.target.closest(".tabs-menu-item")) {
+ return;
+ }
+
+ let tabActive = this.state.tabActive;
+ let tabCount = this.props.children.length;
+
+ switch (event.code) {
+ case "ArrowRight":
+ tabActive = Math.min(tabCount - 1, tabActive + 1);
+ break;
+ case "ArrowLeft":
+ tabActive = Math.max(0, tabActive - 1);
+ break;
+ }
+
+ if (this.state.tabActive != tabActive) {
+ this.setActive(tabActive);
+ }
+ },
+
+ onClickTab: function (index, event) {
+ this.setActive(index);
+ event.preventDefault();
+ },
+
+ onAllTabsMenuClick: function (event) {
+ if (this.props.onAllTabsMenuClick) {
+ this.props.onAllTabsMenuClick(event);
+ }
+ },
+
+ // API
+
+ setActive: function (index) {
+ let onAfterChange = this.props.onAfterChange;
+ let onBeforeChange = this.props.onBeforeChange;
+
+ if (onBeforeChange) {
+ let cancel = onBeforeChange(index);
+ if (cancel) {
+ return;
+ }
+ }
+
+ let created = [...this.state.created];
+ created[index] = true;
+
+ let newState = Object.assign({}, this.state, {
+ tabActive: index,
+ created: created
+ });
+
+ this.setState(newState, () => {
+ // Properly set focus on selected tab.
+ let node = findDOMNode(this);
+ let selectedTab = node.querySelector(".is-active > a");
+ if (selectedTab) {
+ selectedTab.focus();
+ }
+
+ if (onAfterChange) {
+ onAfterChange(index);
+ }
+ });
+ },
+
+ // Rendering
+
+ renderMenuItems: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let tabs = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let ref = ("tab-menu-" + index);
+ let title = tab.props.title;
+ let tabClassName = tab.props.className;
+ let isTabSelected = this.state.tabActive === index;
+
+ let classes = [
+ "tabs-menu-item",
+ tabClassName,
+ isTabSelected ? "is-active" : ""
+ ].join(" ");
+
+ // Set tabindex to -1 (except the selected tab) so, it's focusable,
+ // but not reachable via sequential tab-key navigation.
+ // Changing selected tab (and so, moving focus) is done through
+ // left and right arrow keys.
+ // See also `onKeyDown()` event handler.
+ return (
+ DOM.li({
+ ref: ref,
+ key: index,
+ id: "tab-" + index,
+ className: classes,
+ role: "presentation",
+ },
+ DOM.a({
+ tabIndex: this.state.tabActive === index ? 0 : -1,
+ "aria-controls": "panel-" + index,
+ "aria-selected": isTabSelected,
+ role: "tab",
+ onClick: this.onClickTab.bind(this, index),
+ },
+ title
+ )
+ )
+ );
+ });
+
+ // Display the menu only if there is not enough horizontal
+ // space for all tabs (and overflow happened).
+ let allTabsMenu = this.state.overflow ? (
+ DOM.div({
+ className: "all-tabs-menu",
+ onClick: this.props.onAllTabsMenuClick
+ })
+ ) : null;
+
+ return (
+ DOM.nav({className: "tabs-navigation"},
+ DOM.ul({className: "tabs-menu", role: "tablist"},
+ tabs
+ ),
+ allTabsMenu
+ )
+ );
+ },
+
+ renderPanels: function () {
+ if (!this.props.children) {
+ throw new Error("There must be at least one Tab");
+ }
+
+ if (!Array.isArray(this.props.children)) {
+ this.props.children = [this.props.children];
+ }
+
+ let selectedIndex = this.state.tabActive;
+
+ let panels = this.props.children
+ .map(tab => {
+ return typeof tab === "function" ? tab() : tab;
+ }).filter(tab => {
+ return tab;
+ }).map((tab, index) => {
+ let selected = selectedIndex == index;
+
+ // Use 'visibility:hidden' + 'width/height:0' for hiding
+ // content of non-selected tab. It's faster (not sure why)
+ // than display:none and visibility:collapse.
+ let style = {
+ visibility: selected ? "visible" : "hidden",
+ height: selected ? "100%" : "0",
+ width: selected ? "100%" : "0",
+ };
+
+ return (
+ DOM.div({
+ key: index,
+ id: "panel-" + index,
+ style: style,
+ className: "tab-panel-box",
+ role: "tabpanel",
+ "aria-labelledby": "tab-" + index,
+ },
+ (selected || this.state.created[index]) ? tab : null
+ )
+ );
+ });
+
+ return (
+ DOM.div({className: "panels"},
+ panels
+ )
+ );
+ },
+
+ render: function () {
+ let classNames = ["tabs", this.props.className].join(" ");
+
+ return (
+ DOM.div({className: classNames},
+ this.renderMenuItems(),
+ this.renderPanels()
+ )
+ );
+ },
+ });
+
+ /**
+ * Renders simple tab 'panel'.
+ */
+ let Panel = React.createClass({
+ displayName: "Panel",
+
+ propTypes: {
+ title: React.PropTypes.string.isRequired,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired
+ },
+
+ render: function () {
+ return DOM.div({className: "tab-panel"},
+ this.props.children
+ );
+ }
+ });
+
+ // Exports from this module
+ exports.TabPanel = Panel;
+ exports.Tabs = Tabs;
+});
diff --git a/devtools/client/shared/components/test/browser/.eslintrc.js b/devtools/client/shared/components/test/browser/.eslintrc.js
new file mode 100644
index 000000000..76904829d
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/shared/components/test/browser/browser.ini b/devtools/client/shared/components/test/browser/browser.ini
new file mode 100644
index 000000000..9db9eca66
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_notification_box_basic.js]
diff --git a/devtools/client/shared/components/test/browser/browser_notification_box_basic.js b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
new file mode 100644
index 000000000..b7c6a669b
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_notification_box_basic.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../../../framework/test/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+const TEST_URI = "data:text/html;charset=utf-8,Test page";
+
+/**
+ * Basic test that checks existence of the Notification box.
+ */
+add_task(function* () {
+ info("Test Notification box basic started");
+
+ let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+
+ // Append a notification
+ let notificationBox = toolbox.getNotificationBox();
+ notificationBox.appendNotification(
+ "Info message",
+ "id1",
+ null,
+ notificationBox.PRIORITY_INFO_HIGH
+ );
+
+ // Verify existence of one notification.
+ let parentNode = toolbox.doc.getElementById("toolbox-notificationbox");
+ let nodes = parentNode.querySelectorAll(".notification");
+ is(nodes.length, 1, "There must be one notification");
+});
diff --git a/devtools/client/shared/components/test/mochitest/.eslintrc.js b/devtools/client/shared/components/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..677cbb424
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/shared/components/test/mochitest/chrome.ini b/devtools/client/shared/components/test/mochitest/chrome.ini
new file mode 100644
index 000000000..27a4be137
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_frame_01.html]
+[test_HSplitBox_01.html]
+[test_notification_box_01.html]
+[test_notification_box_02.html]
+[test_notification_box_03.html]
+[test_reps_array.html]
+[test_reps_attribute.html]
+[test_reps_comment-node.html]
+[test_reps_date-time.html]
+[test_reps_document.html]
+[test_reps_element-node.html]
+[test_reps_event.html]
+[test_reps_function.html]
+[test_reps_grip.html]
+[test_reps_grip-array.html]
+[test_reps_grip-map.html]
+[test_reps_infinity.html]
+[test_reps_long-string.html]
+[test_reps_nan.html]
+[test_reps_null.html]
+[test_reps_number.html]
+[test_reps_object.html]
+[test_reps_object-with-text.html]
+[test_reps_object-with-url.html]
+[test_reps_promise.html]
+[test_reps_regexp.html]
+[test_reps_string.html]
+[test_reps_stylesheet.html]
+[test_reps_symbol.html]
+[test_reps_text-node.html]
+[test_reps_undefined.html]
+[test_reps_window.html]
+[test_sidebar_toggle.html]
+[test_stack-trace.html]
+[test_tabs_accessibility.html]
+[test_tabs_menu.html]
+[test_tree_01.html]
+[test_tree_02.html]
+[test_tree_03.html]
+[test_tree_04.html]
+[test_tree_05.html]
+[test_tree_06.html]
+[test_tree_07.html]
+[test_tree_08.html]
+[test_tree_09.html]
+[test_tree_10.html]
+[test_tree_11.html]
diff --git a/devtools/client/shared/components/test/mochitest/head.js b/devtools/client/shared/components/test/mochitest/head.js
new file mode 100644
index 000000000..b66b72814
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/head.js
@@ -0,0 +1,217 @@
+/* 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"}] */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { Assert } = require("resource://testing-common/Assert.jsm");
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+var { DebuggerServer } = require("devtools/server/main");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var { Task } = require("devtools/shared/task");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+
+flags.testing = true;
+var { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/",
+ window
+});
+
+let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+let React = browserRequire("devtools/client/shared/vendor/react");
+var TestUtils = React.addons.TestUtils;
+
+var EXAMPLE_URL = "http://example.com/browser/browser/devtools/shared/test/";
+
+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 setProps(component, newProps) {
+ return new Promise(resolve => {
+ component.setProps(newProps, onNextAnimationFrame(resolve));
+ });
+}
+
+function dumpn(msg) {
+ dump(`SHARED-COMPONENTS-TEST: ${msg}\n`);
+}
+
+/**
+ * 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);
+}
+
+// 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
+}) {
+ let $ = selector => el.querySelector(selector);
+
+ let $func = $(".frame-link-function-display-name");
+ let $source = $(".frame-link-source");
+ let $sourceInner = $(".frame-link-source-inner");
+ let $filename = $(".frame-link-filename");
+ let $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($sourceInner.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`");
+ }
+}
+
+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 = React.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 = TestUtils.createRenderer();
+ renderer.render(el, {});
+ return renderer.getRenderOutput();
+}
+
+/**
+ * Test that a rep renders correctly across different modes.
+ */
+function testRepRenderModes(modeTests, testName, componentUnderTest, gripStub) {
+ modeTests.forEach(({mode, expectedOutput, message}) => {
+ const modeString = typeof mode === "undefined" ? "no mode" : mode;
+ if (!message) {
+ message = `${testName}: ${modeString} renders correctly.`;
+ }
+
+ const rendered = renderComponent(componentUnderTest.rep, { object: gripStub, mode });
+ is(rendered.textContent, expectedOutput, message);
+ });
+}
diff --git a/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
new file mode 100644
index 000000000..7a7187de6
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
@@ -0,0 +1,126 @@
+<!-- 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 type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript "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="resource://devtools/client/themes/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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+const FUDGE_FACTOR = .1;
+function aboutEq(a, b) {
+ dumpn(`Checking ${a} ~= ${b}`);
+ return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = Task.async(function* () {
+ try {
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ let HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/h-split-box"));
+ ok(HSplitBox, "Should get HSplitBox");
+
+ const newSizes = [];
+ const box = ReactDOM.render(HSplitBox({
+ start: "hello!",
+ end: "world!",
+ startWidth: .5,
+ onResize(newSize) {
+ newSizes.push(newSize);
+ },
+ }), window.document.body);
+
+ // 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.
+
+ yield setProps(box, { 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.
+
+ let 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/mochitest/test_frame_01.html b/devtools/client/shared/components/test/mochitest/test_frame_01.html
new file mode 100644
index 000000000..ed3bc90c2
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_frame_01.html
@@ -0,0 +1,309 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Frame = React.createFactory(browserRequire("devtools/client/shared/components/frame"));
+ ok(Frame, "Should get Frame");
+
+ // Check when there's a column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55:10",
+ });
+
+ // Check when there's no column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when column === 0
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 0,
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
+ });
+
+ // Check when there's no parseable URL source;
+ // should not link but should render line/columns
+ yield checkFrameComponent({
+ frame: {
+ source: "self-hosted",
+ line: 1,
+ }
+ }, {
+ file: "self-hosted",
+ line: "1",
+ shouldLink: false,
+ tooltip: "self-hosted:1",
+ });
+ yield 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
+ yield checkFrameComponent({
+ frame: {
+ line: 1,
+ }
+ }, {
+ file: "(unknown)",
+ line: "1",
+ shouldLink: false,
+ tooltip: "(unknown):1",
+ });
+ yield 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
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check when line is 0; this should be an invalid
+ // line option, so don't render line/column
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ column: 55,
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check when source is via Scratchpad; we should render out the
+ // lines and columns as this is linkable.
+ yield checkFrameComponent({
+ frame: {
+ source: "Scratchpad/1",
+ line: 10,
+ column: 50,
+ }
+ }, {
+ file: "Scratchpad/1",
+ line: 10,
+ column: 50,
+ shouldLink: true,
+ tooltip: "View source in Debugger → Scratchpad/1:10:50",
+ });
+
+ // Check that line and column can be strings
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: "10",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ line: 10,
+ column: 55,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://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
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: "0",
+ column: "55",
+ }
+ }, {
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFullSourceUrl option works correctly
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFullSourceUrl: true
+ }, {
+ file: "http://myfile.com/mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that the showFunctionName option works correctly
+ yield checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ }
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ yield checkFrameComponent({
+ frame: {
+ functionDisplayName: "myfun",
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: "myfun",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check that anonymous function name is not displayed unless explicitly enabled
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true
+ }, {
+ functionName: null,
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ yield checkFrameComponent({
+ frame: {
+ source: "http://myfile.com/mahscripts.js",
+ line: 0,
+ },
+ showFunctionName: true,
+ showAnonymousFunctionName: true
+ }, {
+ functionName: "<anonymous>",
+ file: "mahscripts.js",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/mahscripts.js",
+ });
+
+ // Check if file is rendered with "/" for root documents when showEmptyPathAsHost is false
+ yield checkFrameComponent({
+ frame: {
+ source: "http://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: false,
+ }, {
+ file: "/",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://www.cnn.com/:1",
+ });
+
+ // Check if file is rendered with hostname for root documents when showEmptyPathAsHost is true
+ yield checkFrameComponent({
+ frame: {
+ source: "http://www.cnn.com/",
+ line: "1",
+ },
+ showEmptyPathAsHost: true,
+ }, {
+ file: "www.cnn.com",
+ line: "1",
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://www.cnn.com/:1",
+ });
+
+ function* checkFrameComponent(input, expected) {
+ let props = Object.assign({ onClick: () => {} }, input);
+ let frame = ReactDOM.render(Frame(props), window.document.body);
+ yield forceRender(frame);
+
+ let el = frame.getDOMNode();
+ let { source } = input.frame;
+ checkFrameString(Object.assign({ el, source }, 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/mochitest/test_notification_box_01.html b/devtools/client/shared/components/test/mochitest/test_notification_box_01.html
new file mode 100644
index 000000000..947fb9803
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_01.html
@@ -0,0 +1,108 @@
+<!-- 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 a notification
+* Notification priority
+* Closing notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ const renderedBox = shallowRenderComponent(NotificationBox, {});
+ is(renderedBox.type, "div", "NotificationBox is rendered as <div>");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ is(notificationNode.className, "notificationbox",
+ "NotificationBox has expected classname");
+ 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);
+ } 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/mochitest/test_notification_box_02.html b/devtools/client/shared/components/test/mochitest/test_notification_box_02.html
new file mode 100644
index 000000000..ebeb0400d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_02.html
@@ -0,0 +1,70 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let 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");
+
+ let 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/mochitest/test_notification_box_03.html b/devtools/client/shared/components/test/mochitest/test_notification_box_03.html
new file mode 100644
index 000000000..d7fc146fe
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_notification_box_03.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>
+<!--
+Test for Notification Box. The test is checking:
+* Using custom buttons in a notification
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Notification Box</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { NotificationBox, PriorityLevels } = browserRequire("devtools/client/shared/components/notification-box");
+
+ // Test rendering
+ let boxElement = React.createElement(NotificationBox);
+ let notificationBox = TestUtils.renderIntoDocument(boxElement);
+ let notificationNode = ReactDOM.findDOMNode(notificationBox);
+
+ let buttonCallbackExecuted = false;
+ var 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
+ );
+
+ let buttonNodes = notificationNode.querySelectorAll(
+ ".notification-button");
+
+ 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/mochitest/test_reps_array.html b/devtools/client/shared/components/test/mochitest/test_reps_array.html
new file mode 100644
index 000000000..6ad7f2c43
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html
@@ -0,0 +1,259 @@
+<!-- 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 ArrayRep rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ArrayRep</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+/* import-globals-from head.js */
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ArrayRep } = browserRequire("devtools/client/shared/components/reps/array");
+
+ let componentUnderTest = ArrayRep;
+ const maxLength = {
+ short: 3,
+ long: 300
+ };
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanShortMaxProps();
+ yield testMoreThanLongMaxProps();
+ yield testRecursiveArray();
+
+ // Test that properties are rendered as expected by ItemRep
+ yield testNested();
+
+ yield testArray();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test that correct rep is chosen
+ const stub = [];
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ArrayRep.rep,
+ `Rep correctly selects ${ArrayRep.rep.displayName}`);
+
+
+ // Test rendering
+ const defaultOutput = `[]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
+ }
+
+ function testMaxProps() {
+ const stub = [1, "foo", {}];
+ const defaultOutput = `[ 1, "foo", Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanShortMaxProps() {
+ const stub = Array(maxLength.short + 1).fill("foo");
+ const defaultShortOutput = `[ ${Array(maxLength.short).fill("\"foo\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.short + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: `[ ${Array(maxLength.short + 1).fill("\"foo\"").join(", ")} ]`,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanLongMaxProps() {
+ const stub = Array(maxLength.long + 1).fill("foo");
+ const defaultShortOutput = `[ ${Array(maxLength.short).fill("\"foo\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+ const defaultLongOutput = `[ ${Array(maxLength.long).fill("\"foo\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.long + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testRecursiveArray() {
+ let stub = [1];
+ stub.push(stub);
+ const defaultOutput = `[ 1, [2] ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[2]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRecursiveArray", componentUnderTest, stub);
+ }
+
+ function testNested() {
+ let stub = [
+ {
+ p1: "s1",
+ p2: ["a1", "a2", "a3"],
+ p3: "s3",
+ p4: "s4"
+ }
+ ];
+ const defaultOutput = `[ Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[1]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+ }
+
+ function testArray() {
+ let stub = [
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
+ ];
+
+ const defaultOutput = `[ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",` +
+ ` "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",` +
+ ` "u", "v", "w", "x", "y", "z" ]`;
+ const shortOutput = `[ "a", "b", "c", 23 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[26]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNested", componentUnderTest, stub);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_attribute.html b/devtools/client/shared/components/test/mochitest/test_reps_attribute.html
new file mode 100644
index 000000000..aa8a5dfad
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_attribute.html
@@ -0,0 +1,56 @@
+<!-- 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 Attribute rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Attribute</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Attribute } = browserRequire("devtools/client/shared/components/reps/attribute");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn19.obj65",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "value": "autocomplete-suggestions"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Attribute.rep, `Rep correctly selects ${Attribute.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Attribute.rep, { object: gripStub });
+ is(renderedComponent.textContent, "class=\"autocomplete-suggestions\"", "Attribute rep has expected text content");
+ } 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/mochitest/test_reps_comment-node.html b/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html
new file mode 100644
index 000000000..4e03d8b30
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_comment-node.html
@@ -0,0 +1,80 @@
+<!-- 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 comment-node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - comment-node</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { CommentNode } = browserRequire("devtools/client/shared/components/reps/comment-node");
+
+ let gripStub = {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "Comment",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 8,
+ "nodeName": "#comment",
+ "textContent": "test\nand test\nand test\nand test\nand test\nand test\nand test"
+ }
+ };
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, CommentNode.rep,
+ `Rep correctly selects ${CommentNode.rep.displayName}`);
+
+ // Test rendering.
+ const renderedComponent = renderComponent(CommentNode.rep, { object: gripStub });
+ is(renderedComponent.className, "objectBox theme-comment",
+ "CommentNode rep has expected class names");
+ is(renderedComponent.textContent,
+ `<!-- test\nand test\nand test\nan…d test\nand test\nand test -->`,
+ "CommentNode rep has expected text content");
+
+ // Test tiny rendering.
+ const tinyRenderedComponent = renderComponent(CommentNode.rep, {
+ object: gripStub,
+ mode: "tiny"
+ });
+ is(tinyRenderedComponent.textContent,
+ `<!-- test\\nand test\\na… test\\nand test -->`,
+ "CommentNode rep has expected text content in tiny mode");
+
+ // Test long rendering.
+ const longRenderedComponent = renderComponent(CommentNode.rep, {
+ object: gripStub,
+ mode: "long"
+ });
+ is(longRenderedComponent.textContent, `<!-- ${gripStub.preview.textContent} -->`,
+ "CommentNode rep has expected text content in long mode");
+ } 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/mochitest/test_reps_date-time.html b/devtools/client/shared/components/test/mochitest/test_reps_date-time.html
new file mode 100644
index 000000000..a82783b6b
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_date-time.html
@@ -0,0 +1,79 @@
+<!-- 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 DateTime rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - DateTime</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { DateTime } = browserRequire("devtools/client/shared/components/reps/date-time");
+
+ try {
+ testValid();
+ testInvalid();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testValid() {
+ let gripStub = {
+ "type": "object",
+ "class": "Date",
+ "actor": "server1.conn0.child1/obj32",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": 1459372644859
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, DateTime.rep, `Rep correctly selects ${DateTime.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
+ is(renderedComponent.textContent, "2016-03-30T21:17:24.859Z", "DateTime rep has expected text content for valid date");
+ }
+
+ function testInvalid() {
+ let gripStub = {
+ "type": "object",
+ "actor": "server1.conn0.child1/obj32",
+ "class": "Date",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "timestamp": {
+ "type": "NaN"
+ }
+ }
+ };
+
+ // Test rendering
+ const renderedComponent = renderComponent(DateTime.rep, { object: gripStub });
+ is(renderedComponent.textContent, "Invalid Date", "DateTime rep has expected text content for invalid date");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_document.html b/devtools/client/shared/components/test/mochitest/test_reps_document.html
new file mode 100644
index 000000000..2afabca44
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_document.html
@@ -0,0 +1,56 @@
+<!-- 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 Document rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Document</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Document } = browserRequire("devtools/client/shared/components/reps/document");
+
+ try {
+ let gripStub = {
+ "type": "object",
+ "class": "HTMLDocument",
+ "actor": "server1.conn17.obj115",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 9,
+ "nodeName": "#document",
+ "location": "https://www.mozilla.org/en-US/firefox/new/"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Document.rep, `Rep correctly selects ${Document.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Document.rep, { object: gripStub });
+ is(renderedComponent.textContent, "https://www.mozilla.org/en-US/firefox/new/", "Document rep has expected text content");
+ } 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/mochitest/test_reps_element-node.html b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
new file mode 100644
index 000000000..d4e22c7ab
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
@@ -0,0 +1,341 @@
+<!-- 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 Element node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Element node</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ElementNode } = browserRequire("devtools/client/shared/components/reps/element-node");
+
+ try {
+ yield testBodyNode();
+ yield testDocumentElement();
+ yield testNode();
+ yield testNodeWithLeadingAndTrailingSpacesClassName();
+ yield testNodeWithoutAttributes();
+ yield testLotsOfAttributes();
+ yield testSvgNode();
+ yield testSvgNodeInXHTML();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBodyNode() {
+ const stub = getGripStub("testBodyNode");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for body node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, `<body id="body-id" class="body-class">`,
+ "Element node rep has expected text content for body node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `body#body-id.body-class`,
+ "Element node rep has expected text content for body node in tiny mode");
+ }
+
+ function testDocumentElement() {
+ const stub = getGripStub("testDocumentElement");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for document element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, `<html dir="ltr" lang="en-US">`,
+ "Element node rep has expected text content for document element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `html`,
+ "Element node rep has expected text content for document element in tiny mode");
+ }
+
+ function testNode() {
+ const stub = getGripStub("testNode");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ `<input id="newtab-customize-button" class="bar baz" dir="ltr" ` +
+ `title="Customize your New Tab page" value="foo" type="button">`,
+ "Element node rep has expected text content for element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent,
+ `input#newtab-customize-button.bar.baz`,
+ "Element node rep has expected text content for element node in tiny mode");
+ }
+
+ function testNodeWithLeadingAndTrailingSpacesClassName() {
+ const stub = getGripStub("testNodeWithLeadingAndTrailingSpacesClassName");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ `<body id="nightly-whatsnew" class=" html-ltr ">`,
+ "Element node rep output element node with the class trailing spaces");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent,
+ `body#nightly-whatsnew.html-ltr`,
+ "Element node rep does not show leading nor trailing spaces " +
+ "on class attribute in tiny mode");
+ }
+
+ function testNodeWithoutAttributes() {
+ const stub = getGripStub("testNodeWithoutAttributes");
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent, "<p>",
+ "Element node rep has expected text content for element node without attributes");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `p`,
+ "Element node rep has expected text content for element node without attributes");
+ }
+
+ function testLotsOfAttributes() {
+ const stub = getGripStub("testLotsOfAttributes");
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+ 'h="" i="" j="" k="" l="" m="" n="">',
+ "Element node rep has expected text content for node with lots of attributes");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `p#lots-of-attributes`,
+ "Element node rep has expected text content for node in tiny mode");
+ }
+
+ function testSvgNode() {
+ const stub = getGripStub("testSvgNode");
+
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for SVG element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<clipPath id="clip" class="svg-element">',
+ "Element node rep has expected text content for SVG element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `clipPath#clip.svg-element`,
+ "Element node rep has expected text content for SVG element node in tiny mode");
+ }
+
+ function testSvgNodeInXHTML() {
+ const stub = getGripStub("testSvgNodeInXHTML");
+
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, ElementNode.rep,
+ `Rep correctly selects ${ElementNode.rep.displayName} for XHTML SVG element node`);
+
+ const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+ is(renderedComponent.textContent,
+ '<svg:circle class="svg-element" cx="0" cy="0" r="5">',
+ "Element node rep has expected text content for XHTML SVG element node");
+
+ const tinyRenderedComponent = renderComponent(
+ ElementNode.rep, { object: stub, mode: "tiny" });
+ is(tinyRenderedComponent.textContent, `svg:circle.svg-element`,
+ "Element node rep has expected text content for XHTML SVG element in tiny mode");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testBodyNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj30",
+ "class": "HTMLBodyElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {
+ "class": "body-class",
+ "id": "body-id"
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testDocumentElement":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj40",
+ "class": "HTMLHtmlElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "html",
+ "attributes": {
+ "dir": "ltr",
+ "lang": "en-US"
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn2.child1/obj116",
+ "class": "HTMLInputElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "input",
+ "attributes": {
+ "id": "newtab-customize-button",
+ "dir": "ltr",
+ "title": "Customize your New Tab page",
+ "class": "bar baz",
+ "value": "foo",
+ "type": "button"
+ },
+ "attributesLength": 6
+ }
+ };
+ case "testNodeWithLeadingAndTrailingSpacesClassName":
+ return {
+ "type": "object",
+ "actor": "server1.conn3.child1/obj59",
+ "class": "HTMLBodyElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {
+ "id": "nightly-whatsnew",
+ "class": " html-ltr "
+ },
+ "attributesLength": 2
+ }
+ };
+ case "testNodeWithoutAttributes":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj32",
+ "class": "HTMLParagraphElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "p",
+ "attributes": {},
+ "attributesLength": 1
+ }
+ };
+ case "testLotsOfAttributes":
+ return {
+ "type": "object",
+ "actor": "server1.conn2.child1/obj30",
+ "class": "HTMLParagraphElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "p",
+ "attributes": {
+ "id": "lots-of-attributes",
+ "a": "",
+ "b": "",
+ "c": "",
+ "d": "",
+ "e": "",
+ "f": "",
+ "g": "",
+ "h": "",
+ "i": "",
+ "j": "",
+ "k": "",
+ "l": "",
+ "m": "",
+ "n": ""
+ },
+ "attributesLength": 15
+ }
+ };
+ case "testSvgNode":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj42",
+ "class": "SVGClipPathElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "clipPath",
+ "attributes": {
+ "id": "clip",
+ "class": "svg-element"
+ },
+ "attributesLength": 0
+ }
+ };
+ case "testSvgNodeInXHTML":
+ return {
+ "type": "object",
+ "actor": "server1.conn3.child1/obj34",
+ "class": "SVGCircleElement",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "svg:circle",
+ "attributes": {
+ "class": "svg-element",
+ "cx": "0",
+ "cy": "0",
+ "r": "5"
+ },
+ "attributesLength": 3
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_event.html b/devtools/client/shared/components/test/mochitest/test_reps_event.html
new file mode 100644
index 000000000..7dfe72d6f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_event.html
@@ -0,0 +1,300 @@
+<!-- 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 Event rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Event</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Event } = browserRequire("devtools/client/shared/components/reps/event");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testEvent") });
+ is(renderedRep.type, Event.rep, `Rep correctly selects ${Event.rep.displayName}`);
+
+ yield testEvent();
+ yield testMouseEvent();
+ yield testKeyboardEvent();
+ yield testMessageEvent();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testEvent") });
+ is(renderedComponent.textContent,
+ "Event { isTrusted: true, eventPhase: 2, bubbles: false, 7 more… }",
+ "Event rep has expected text content for an event");
+ }
+
+ function testMouseEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMouseEvent") });
+ is(renderedComponent.textContent,
+ "MouseEvent { clientX: 62, clientY: 18, layerX: 0, 2 more… }",
+ "Event rep has expected text content for a mouse event");
+ }
+
+ function testKeyboardEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testKeyboardEvent") });
+ is(renderedComponent.textContent,
+ "KeyboardEvent { key: \"Control\", charCode: 0, keyCode: 17 }",
+ "Event rep has expected text content for a keyboard event");
+ }
+
+ function testMessageEvent() {
+ const renderedComponent = renderComponent(Event.rep, { object: getGripStub("testMessageEvent") });
+ is(renderedComponent.textContent,
+ "MessageEvent { isTrusted: false, data: \"test data\", origin: \"null\", 7 more… }",
+ "Event rep has expected text content for a message event");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testEvent":
+ return {
+ "type": "object",
+ "class": "Event",
+ "actor": "server1.conn23.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "beforeprint",
+ "properties": {
+ "isTrusted": true,
+ "currentTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj37",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "eventPhase": 2,
+ "bubbles": false,
+ "cancelable": false,
+ "defaultPrevented": false,
+ "timeStamp": 1466780008434005,
+ "originalTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj38",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "explicitOriginalTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ },
+ "NONE": 0
+ },
+ "target": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn23.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "http://example.com"
+ }
+ }
+ }
+ };
+
+ case "testMouseEvent":
+ return {
+ "type": "object",
+ "class": "MouseEvent",
+ "actor": "server1.conn20.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "click",
+ "properties": {
+ "buttons": 0,
+ "clientX": 62,
+ "clientY": 18,
+ "layerX": 0,
+ "layerY": 0
+ },
+ "target": {
+ "type": "object",
+ "class": "HTMLDivElement",
+ "actor": "server1.conn20.obj40",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "div",
+ "attributes": {
+ "id": "test"
+ },
+ "attributesLength": 1
+ }
+ }
+ }
+ };
+
+ case "testKeyboardEvent":
+ return {
+ "type": "object",
+ "class": "KeyboardEvent",
+ "actor": "server1.conn21.obj49",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "keyup",
+ "properties": {
+ "key": "Control",
+ "charCode": 0,
+ "keyCode": 17
+ },
+ "target": {
+ "type": "object",
+ "class": "HTMLBodyElement",
+ "actor": "server1.conn21.obj50",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "body",
+ "attributes": {},
+ "attributesLength": 0
+ }
+ },
+ "eventKind": "key",
+ "modifiers": []
+ }
+ };
+
+ case "testMessageEvent":
+ return {
+ "type": "object",
+ "class": "MessageEvent",
+ "actor": "server1.conn3.obj34",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "DOMEvent",
+ "type": "message",
+ "properties": {
+ "isTrusted": false,
+ "data": "test data",
+ "origin": "null",
+ "lastEventId": "",
+ "source": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ },
+ "ports": {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj37",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0
+ },
+ "currentTarget": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj38",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ },
+ "eventPhase": 2,
+ "bubbles": false,
+ "cancelable": false
+ },
+ "target": {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 760,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": ""
+ }
+ }
+ }
+ };
+
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_function.html b/devtools/client/shared/components/test/mochitest/test_reps_function.html
new file mode 100644
index 000000000..ede694329
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_function.html
@@ -0,0 +1,206 @@
+<!-- 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 Func rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Func</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Func } = browserRequire("devtools/client/shared/components/reps/function");
+
+ const componentUnderTest = Func;
+
+ try {
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testNamed");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Func.rep, `Rep correctly selects ${Func.rep.displayName}`);
+
+ yield testNamed();
+ yield testVarNamed();
+ yield testAnon();
+ yield testLongName();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testNamed() {
+ // Test declaration: `function testName{ let innerVar = "foo" }`
+ const testName = "testNamed";
+
+ const defaultOutput = `testName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUserNamed() {
+ // Test declaration: `function testName{ let innerVar = "foo" }`
+ const testName = "testUserNamed";
+
+ const defaultOutput = `testUserName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testVarNamed() {
+ // Test declaration: `let testVarName = function() { }`
+ const testName = "testVarNamed";
+
+ const defaultOutput = `testVarName()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testAnon() {
+ // Test declaration: `() => {}`
+ const testName = "testAnon";
+
+ const defaultOutput = `function()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testLongName() {
+ // Test declaration: `let f = function loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong() { }`
+ const testName = "testLongName";
+
+ const defaultOutput = `looooooooooooooooooooooooooooooooooooooooooooooooo\u2026ooooooooooooooooooooooooooooooooooooooooooooong()`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn6.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "testName",
+ "displayName": "testName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testUserNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn6.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "testName",
+ "userDisplayName": "testUserName",
+ "displayName": "testName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testVarNamed":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj41",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "displayName": "testVarName",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testAnon":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj45",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+
+ case "testLongName":
+ return {
+ "type": "object",
+ "class": "Function",
+ "actor": "server1.conn7.obj67",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "name": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ "displayName": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ "location": {
+ "url": "debugger eval code",
+ "line": 1
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
new file mode 100644
index 000000000..db4f0296e
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -0,0 +1,707 @@
+<!-- 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 GripArray rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripArray</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { GripArray } = browserRequire("devtools/client/shared/components/reps/grip-array");
+
+ let componentUnderTest = GripArray;
+ const maxLength = {
+ short: 3,
+ long: 300
+ };
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanShortMaxProps();
+ yield testMoreThanLongMaxProps();
+ yield testRecursiveArray();
+ yield testPreviewLimit();
+ yield testNamedNodeMap();
+ yield testNodeList();
+ yield testDocumentFragment();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test array: `[]`
+ const testName = "testBasic";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testBasic");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Array []`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxProps() {
+ // Test array: `[1, "foo", {}]`;
+ const testName = "testMaxProps";
+
+ const defaultOutput = `Array [ 1, "foo", Object ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanShortMaxProps() {
+ // Test array = `["test string"…] //4 items`
+ const testName = "testMoreThanShortMaxProps";
+
+ const defaultOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.short + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: `Array [ ${Array(maxLength.short + 1).fill("\"test string\"").join(", ")} ]`,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanLongMaxProps() {
+ // Test array = `["test string"…] //301 items`
+ const testName = "testMoreThanLongMaxProps";
+
+ const defaultShortOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+ const defaultLongOutput = `Array [ ${Array(maxLength.long).fill("\"test string\"").join(", ")}, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[${maxLength.long + 1}]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testRecursiveArray() {
+ // Test array = `let a = []; a = [a]`
+ const testName = "testRecursiveArray";
+
+ const defaultOutput = `Array [ [1] ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[1]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testPreviewLimit() {
+ const testName = "testPreviewLimit";
+
+ const shortOutput = `Array [ 0, 1, 2, 8 more… ]`;
+ const defaultOutput = `Array [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1 more… ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[11]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: shortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNamedNodeMap() {
+ const testName = "testNamedNodeMap";
+
+ const defaultOutput = `NamedNodeMap [ class="myclass", cellpadding="7", border="3" ]`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNodeList() {
+ const testName = "testNodeList";
+ const defaultOutput = "NodeList [ button#btn-1.btn.btn-log, " +
+ "button#btn-2.btn.btn-err, button#btn-3.btn.btn-count ]";
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[3]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testDocumentFragment() {
+ const testName = "testDocumentFragment";
+
+ const defaultOutput = "DocumentFragment [ li#li-0.list-element, " +
+ "li#li-1.list-element, li#li-2.list-element, 2 more… ]";
+
+ const longOutput = "DocumentFragment [ " +
+ "li#li-0.list-element, li#li-1.list-element, li#li-2.list-element, " +
+ "li#li-3.list-element, li#li-4.list-element ]";
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `[5]`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testBasic":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn0.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 0,
+ "items": []
+ }
+ };
+
+ case "testMaxProps":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ 1,
+ "foo",
+ {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn1.obj36",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0
+ }
+ ]
+ }
+ };
+
+ case "testMoreThanShortMaxProps":
+ let shortArrayGrip = {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": maxLength.short + 1,
+ "items": []
+ }
+ };
+
+ // Generate array grip with length 4, which is more that the maximum
+ // limit in case of the 'short' mode.
+ for (let i = 0; i < maxLength.short + 1; i++) {
+ shortArrayGrip.preview.items.push("test string");
+ }
+
+ return shortArrayGrip;
+
+ case "testMoreThanLongMaxProps":
+ let longArrayGrip = {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": maxLength.long + 1,
+ "items": []
+ }
+ };
+
+ // Generate array grip with length 301, which is more that the maximum
+ // limit in case of the 'long' mode.
+ for (let i = 0; i < maxLength.long + 1; i++) {
+ longArrayGrip.preview.items.push("test string");
+ }
+
+ return longArrayGrip;
+
+ case "testPreviewLimit":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn1.obj31",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 12,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 11,
+ "items": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ }
+ };
+
+ case "testRecursiveArray":
+ return {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj42",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 1,
+ "items": [
+ {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn3.obj43",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 1
+ }
+ }
+ ]
+ }
+ };
+
+ case "testNamedNodeMap":
+ return {
+ "type": "object",
+ "class": "NamedNodeMap",
+ "actor": "server1.conn3.obj42",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 6,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj43",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "class",
+ "value": "myclass"
+ }
+ },
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj44",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "cellpadding",
+ "value": "7"
+ }
+ },
+ {
+ "type": "object",
+ "class": "Attr",
+ "actor": "server1.conn3.obj44",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 2,
+ "nodeName": "border",
+ "value": "3"
+ }
+ }
+ ]
+ }
+ };
+
+ case "testNodeList":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj51",
+ "class": "NodeList",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 3,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3,
+ "items": [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj52",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-1",
+ "class": "btn btn-log",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj53",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-2",
+ "class": "btn btn-err",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj54",
+ "class": "HTMLButtonElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "button",
+ "attributes": {
+ "id": "btn-3",
+ "class": "btn btn-count",
+ "type": "button"
+ },
+ "attributesLength": 3
+ }
+ }
+ ]
+ }
+ };
+
+ case "testDocumentFragment":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj45",
+ "class": "DocumentFragment",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 11,
+ "nodeName": "#document-fragment",
+ "childNodesLength": 5,
+ "childNodes": [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj46",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-0",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-1",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-2",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-3",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ },
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj50",
+ "class": "HTMLLIElement",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "DOMNode",
+ "nodeType": 1,
+ "nodeName": "li",
+ "attributes": {
+ "id": "li-4",
+ "class": "list-element"
+ },
+ "attributesLength": 2
+ }
+ }
+ ]
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
new file mode 100644
index 000000000..18470367c
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
@@ -0,0 +1,405 @@
+<!-- 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 GripMap rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripMap</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { GripMap } = browserRequire("devtools/client/shared/components/reps/grip-map");
+
+ const componentUnderTest = GripMap;
+
+ try {
+ yield testEmptyMap();
+ yield testSymbolKeyedMap();
+ yield testWeakMap();
+
+ // // Test entries iterator
+ yield testMaxEntries();
+ yield testMoreThanMaxEntries();
+ yield testUninterestingEntries();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEmptyMap() {
+ // Test object: `new Map()`
+ const testName = "testEmptyMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testEmptyMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Map { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSymbolKeyedMap() {
+ // Test object:
+ // `new Map([[Symbol("a"), "value-a"], [Symbol("b"), "value-b"]])`
+ const testName = "testSymbolKeyedMap";
+
+ const defaultOutput = `Map { Symbol(a): "value-a", Symbol(b): "value-b" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testWeakMap() {
+ // Test object: `new WeakMap([[{a: "key-a"}, "value-a"]])`
+ const testName = "testWeakMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testWeakMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `WeakMap { Object: "value-a" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "WeakMap",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxEntries() {
+ // Test object:
+ // `new Map([["key-a","value-a"], ["key-b","value-b"], ["key-c","value-c"]])`
+ const testName = "testMaxEntries";
+
+ const defaultOutput = `Map { key-a: "value-a", key-b: "value-b", key-c: "value-c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxEntries() {
+ // Test object = `new Map(
+ // [["key-0", "value-0"], ["key-1", "value-1"]], …, ["key-100", "value-100"]]}`
+ const testName = "testMoreThanMaxEntries";
+
+ const defaultOutput =
+ `Map { key-0: "value-0", key-1: "value-1", key-2: "value-2", 98 more… }`;
+
+ // Generate string with 101 entries, which is the max limit for 'long' mode.
+ let longString = Array.from({length: 100}).map((_, i) => `key-${i}: "value-${i}"`);
+ const longOutput = `Map { ${longString.join(", ")}, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingEntries() {
+ // Test object:
+ // `new Map([["key-a",null], ["key-b",undefined], ["key-c","value-c"], ["key-d",4]])`
+ const testName = "testUninterestingEntries";
+
+ const defaultOutput =
+ `Map { key-a: null, key-c: "value-c", key-d: 4, 1 more… }`;
+ const longOutput =
+ `Map { key-a: null, key-b: undefined, key-c: "value-c", key-d: 4 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testEmptyMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj97",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 0,
+ "entries": []
+ }
+ };
+
+ case "testSymbolKeyedMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj118",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 2,
+ "entries": [
+ [
+ {
+ "type": "symbol",
+ "name": "a"
+ },
+ "value-a"
+ ],
+ [
+ {
+ "type": "symbol",
+ "name": "b"
+ },
+ "value-b"
+ ]
+ ]
+ }
+ };
+
+ case "testWeakMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj115",
+ "class": "WeakMap",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 1,
+ "entries": [
+ [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj116",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ },
+ "value-a"
+ ]
+ ]
+ }
+ };
+
+ case "testMaxEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj109",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 3,
+ "entries": [
+ [
+ "key-a",
+ "value-a"
+ ],
+ [
+ "key-b",
+ "value-b"
+ ],
+ [
+ "key-c",
+ "value-c"
+ ]
+ ]
+ }
+ };
+
+ case "testMoreThanMaxEntries": {
+ let entryNb = 101;
+ return {
+ "type": "object",
+ "class": "Map",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": entryNb,
+ // Generate 101 entries, which is more that the maximum
+ // limit in case of the 'long' mode.
+ "entries": Array.from({length: entryNb}).map((_, i) => {
+ return [`key-${i}`, `value-${i}`];
+ })
+ }
+ };
+ }
+
+ case "testUninterestingEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj111",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 4,
+ "entries": [
+ [
+ "key-a",
+ {
+ "type": "null"
+ }
+ ],
+ [
+ "key-b",
+ {
+ "type": "undefined"
+ }
+ ],
+ [
+ "key-c",
+ "value-c"
+ ],
+ [
+ "key-d",
+ 4
+ ]
+ ]
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_grip.html b/devtools/client/shared/components/test/mochitest/test_reps_grip.html
new file mode 100644
index 000000000..15d4e1d25
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip.html
@@ -0,0 +1,887 @@
+<!-- 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 grip rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - grip</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Grip } = browserRequire("devtools/client/shared/components/reps/grip");
+
+ const componentUnderTest = Grip;
+
+ try {
+ yield testBasic();
+ yield testBooleanObject();
+ yield testNumberObject();
+ yield testStringObject();
+ yield testProxy();
+ yield testArrayBuffer();
+ yield testSharedArrayBuffer();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanMaxProps();
+ yield testUninterestingProps();
+ yield testNonEnumerableProps();
+
+ // Test that properties are rendered as expected by PropRep
+ yield testNestedObject();
+ yield testNestedArray();
+
+ // Test that 'more' property doesn't clobber the caption.
+ yield testMoreProp();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ // Test object: `{}`
+ const testName = "testBasic";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testBasic");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testBooleanObject() {
+ // Test object: `new Boolean(true)`
+ const testName = "testBooleanObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Boolean { true }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Boolean`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNumberObject() {
+ // Test object: `new Number(42)`
+ const testName = "testNumberObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Number { 42 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Number`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testStringObject() {
+ // Test object: `new String("foo")`
+ const testName = "testStringObject";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `String { "foo" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `String`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testProxy() {
+ // Test object: `new Proxy({a:1},[1,2,3])`
+ const testName = "testProxy";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Proxy { <target>: Object, <handler>: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Proxy`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testArrayBuffer() {
+ // Test object: `new ArrayBuffer(10)`
+ const testName = "testArrayBuffer";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `ArrayBuffer { byteLength: 10 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `ArrayBuffer`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSharedArrayBuffer() {
+ // Test object: `new SharedArrayBuffer(5)`
+ const testName = "testSharedArrayBuffer";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub(testName);
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `SharedArrayBuffer { byteLength: 5 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `SharedArrayBuffer`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxProps() {
+ // Test object: `{a: "a", b: "b", c: "c"}`;
+ const testName = "testMaxProps";
+
+ const defaultOutput = `Object { a: "a", b: "b", c: "c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxProps() {
+ // Test object = `{p0: "0", p1: "1", p2: "2", …, p100: "100"}`
+ const testName = "testMoreThanMaxProps";
+
+ const defaultOutput = `Object { p0: "0", p1: "1", p2: "2", 98 more… }`;
+
+ // Generate string with 100 properties, which is the max limit
+ // for 'long' mode.
+ let props = "";
+ for (let i = 0; i < 100; i++) {
+ props += "p" + i + ": \"" + i + "\", ";
+ }
+
+ const longOutput = `Object { ${props}1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingProps() {
+ // Test object: `{a: undefined, b: undefined, c: "c", d: 1}`
+ // @TODO This is not how we actually want the preview to be output.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1276376
+ const expectedOutput = `Object { a: undefined, b: undefined, c: "c", 1 more… }`;
+ }
+
+ function testNonEnumerableProps() {
+ // Test object: `Object.defineProperty({}, "foo", {enumerable : false});`
+ const testName = "testNonEnumerableProps";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testNonEnumerableProps");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Grip.rep, `Rep correctly selects ${Grip.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNestedObject() {
+ // Test object: `{objProp: {id: 1}, strProp: "test string"}`
+ const testName = "testNestedObject";
+
+ const defaultOutput = `Object { objProp: Object, strProp: "test string" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testNestedArray() {
+ // Test object: `{arrProp: ["foo", "bar", "baz"]}`
+ const testName = "testNestedArray";
+
+ const defaultOutput = `Object { arrProp: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreProp() {
+ // Test object: `{a: undefined, b: 1, more: 2, d: 3}`;
+ const testName = "testMoreProp";
+
+ const defaultOutput = `Object { b: 1, more: 2, d: 3, 1 more… }`;
+ const longOutput = `Object { a: undefined, b: 1, more: 2, d: 3 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testBasic":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj304",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testMaxProps":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj337",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 3,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "a"
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "b"
+ },
+ "c": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ }
+ },
+ "ownPropertiesLength": 3,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testMoreThanMaxProps": {
+ let grip = {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 101,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 101,
+ "safeGetterValues": {}
+ }
+ };
+
+ // Generate 101 properties, which is more that the maximum
+ // limit in case of the 'long' mode.
+ for (let i = 0; i < 101; i++) {
+ grip.preview.ownProperties["p" + i] = {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": i + ""
+ };
+ }
+
+ return grip;
+ }
+
+ case "testUninterestingProps":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj342",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "c": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "c"
+ },
+ "d": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ }
+ },
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {}
+ }
+ };
+ case "testNonEnumerableProps":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj30",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ }
+ };
+ case "testNestedObject":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj145",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "objProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj146",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ }
+ },
+ "strProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": "test string"
+ }
+ },
+ "ownPropertiesLength": 2,
+ "safeGetterValues": {}
+ }
+ };
+
+ case "testNestedArray":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj326",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "arrProp": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "object",
+ "class": "Array",
+ "actor": "server1.conn0.obj327",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 1,
+ "safeGetterValues": {}
+ },
+ };
+
+ case "testMoreProp":
+ return {
+ "type": "object",
+ "class": "Object",
+ "actor": "server1.conn0.obj342",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "a": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": {
+ "type": "undefined"
+ }
+ },
+ "b": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 1
+ },
+ "more": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 2
+ },
+ "d": {
+ "configurable": true,
+ "enumerable": true,
+ "writable": true,
+ "value": 3
+ }
+ },
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {}
+ }
+ };
+ case "testBooleanObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj57",
+ "class": "Boolean",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {},
+ "wrappedValue": true
+ }
+ };
+ case "testNumberObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj59",
+ "class": "Number",
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {},
+ "wrappedValue": 42
+ }
+ };
+ case "testStringObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj61",
+ "class": "String",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 4,
+ "safeGetterValues": {},
+ "wrappedValue": "foo"
+ }
+ };
+ case "testProxy":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj47",
+ "class": "Proxy",
+ "proxyTarget": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "Object",
+ "ownPropertyLength": 1
+ },
+ "proxyHandler": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {
+ "<target>": {
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj48",
+ "class": "Object",
+ "ownPropertyLength": 1
+ }
+ },
+ "<handler>": {
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj49",
+ "class": "Array",
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ }
+ }
+ },
+ "ownPropertiesLength": 2
+ }
+ };
+ case "testArrayBuffer":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj170",
+ "class": "ArrayBuffer",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {
+ "byteLength": {
+ "getterValue": 10,
+ "getterPrototypeLevel": 1,
+ "enumerable": false,
+ "writable": true
+ }
+ }
+ }
+ };
+ case "testSharedArrayBuffer":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj171",
+ "class": "SharedArrayBuffer",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {
+ "byteLength": {
+ "getterValue": 5,
+ "getterPrototypeLevel": 1,
+ "enumerable": false,
+ "writable": true
+ }
+ }
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_infinity.html b/devtools/client/shared/components/test/mochitest/test_reps_infinity.html
new file mode 100644
index 000000000..e3a7e871f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_infinity.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 Infinity rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Infinity</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { InfinityRep } = browserRequire("devtools/client/shared/components/reps/infinity");
+
+ try {
+ yield testInfinity();
+ yield testNegativeInfinity();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testInfinity() {
+ const stub = getGripStub("testInfinity");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, InfinityRep.rep,
+ `Rep correctly selects ${InfinityRep.rep.displayName} for Infinity value`);
+
+ const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
+ is(renderedComponent.textContent, "Infinity",
+ "Infinity rep has expected text content for Infinity");
+ }
+
+ function testNegativeInfinity() {
+ const stub = getGripStub("testNegativeInfinity");
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, InfinityRep.rep,
+ `Rep correctly selects ${InfinityRep.rep.displayName} for negative Infinity value`);
+
+ const renderedComponent = renderComponent(InfinityRep.rep, { object: stub });
+ is(renderedComponent.textContent, "-Infinity",
+ "Infinity rep has expected text content for negative Infinity");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testInfinity":
+ return {
+ type: "Infinity"
+ };
+ case "testNegativeInfinity":
+ return {
+ type: "-Infinity"
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_long-string.html b/devtools/client/shared/components/test/mochitest/test_reps_long-string.html
new file mode 100644
index 000000000..3caaac913
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_long-string.html
@@ -0,0 +1,125 @@
+<!-- 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 LongString rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - LongString</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { LongStringRep } = browserRequire("devtools/client/shared/components/reps/long-string");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
+ is(renderedRep.type, LongStringRep.rep,
+ `Rep correctly selects ${LongStringRep.rep.displayName}`);
+
+ // Test rendering
+ yield testMultiline();
+ yield testMultilineOpen();
+ yield testFullText();
+ yield testMultilineLimit();
+ yield testUseQuotes();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testMultiline() {
+ const stub = getGripStub("testMultiline");
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: stub });
+
+ is(renderedComponent.textContent, `"${stub.initial}…"`,
+ "LongString rep has expected text content for multiline string");
+ }
+
+ function testMultilineLimit() {
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
+
+ is(
+ renderedComponent.textContent,
+ `"a\naaaaaaaaaaaaaaaaaa…"`,
+ "LongString rep has expected text content for multiline string " +
+ "with specified number of characters");
+ }
+
+ function testMultilineOpen() {
+ const stub = getGripStub("testMultiline");
+ const renderedComponent = renderComponent(
+ LongStringRep.rep, { object: stub, member: {open: true}, cropLimit: 20 });
+
+ is(renderedComponent.textContent, `"${stub.initial}…"`,
+ "LongString rep has expected text content for multiline string when open");
+ }
+
+ function testFullText() {
+ const stub = getGripStub("testFullText");
+ const renderedComponentOpen = renderComponent(
+ LongStringRep.rep, { object: stub, member: {open: true}, cropLimit: 20 });
+
+ is(renderedComponentOpen.textContent, `"${stub.fullText}"`,
+ "LongString rep has expected text content when grip has a fullText " +
+ "property and is open");
+
+ const renderedComponentNotOpen = renderComponent(
+ LongStringRep.rep, { object: stub, cropLimit: 20 });
+
+ is(renderedComponentNotOpen.textContent,
+ `"a\naaaaaaaaaaaaaaaaaa…"`,
+ "LongString rep has expected text content when grip has a fullText " +
+ "property and is not open");
+ }
+
+ function testUseQuotes() {
+ const renderedComponent = renderComponent(LongStringRep.rep,
+ { object: getGripStub("testMultiline"), cropLimit: 20, useQuotes: false });
+
+ is(renderedComponent.textContent,
+ "a\naaaaaaaaaaaaaaaaaa…",
+ "LongString rep was expected to omit quotes");
+ }
+
+ function getGripStub(name) {
+ const multilineFullText = "a\n" + Array(20000).fill("a").join("");
+ const fullTextLength = multilineFullText.length;
+ const initialText = multilineFullText.substring(0, 10000);
+
+ switch (name) {
+ case "testMultiline":
+ return {
+ "type": "longString",
+ "initial": initialText,
+ "length": fullTextLength,
+ "actor": "server1.conn1.child1/longString58"
+ };
+ case "testFullText":
+ return {
+ "type": "longString",
+ "fullText": multilineFullText,
+ "initial": initialText,
+ "length": fullTextLength,
+ "actor": "server1.conn1.child1/longString58"
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_nan.html b/devtools/client/shared/components/test/mochitest/test_reps_nan.html
new file mode 100644
index 000000000..35dc5a08f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_nan.html
@@ -0,0 +1,48 @@
+<!-- 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 NaN rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - NaN</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { NaNRep } = browserRequire("devtools/client/shared/components/reps/nan");
+
+ try {
+ yield testNaN();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testNaN() {
+ const stub = {
+ type: "NaN"
+ };
+ const renderedRep = shallowRenderComponent(Rep, {object: stub});
+ is(renderedRep.type, NaNRep.rep,
+ `Rep correctly selects ${NaNRep.rep.displayName} for NaN value`);
+
+ const renderedComponent = renderComponent(NaNRep.rep, {object: stub});
+ is(renderedComponent.textContent, "NaN", "NaN rep has expected text content");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_null.html b/devtools/client/shared/components/test/mochitest/test_reps_null.html
new file mode 100644
index 000000000..99a06fed1
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_null.html
@@ -0,0 +1,44 @@
+<!-- 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 Null rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Null</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Null } = browserRequire("devtools/client/shared/components/reps/null");
+
+ let gripStub = {
+ "type": "null"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Null.rep, `Rep correctly selects ${Null.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Null.rep, { object: gripStub });
+ is(renderedComponent.textContent, "null", "Null rep has expected text content");
+ } 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/mochitest/test_reps_number.html b/devtools/client/shared/components/test/mochitest/test_reps_number.html
new file mode 100644
index 000000000..50f91d8b0
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_number.html
@@ -0,0 +1,97 @@
+<!-- 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 Number rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Number</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Number } = browserRequire("devtools/client/shared/components/reps/number");
+
+ try {
+ yield testInt();
+ yield testBoolean();
+ yield testNegativeZero();
+ yield testUnsafeInt();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+
+ function testInt() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testInt") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for integer value`);
+
+ const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testInt") });
+ is(renderedComponent.textContent, "5", "Number rep has expected text content for integer");
+ }
+
+ function testBoolean() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testTrue") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for boolean value`);
+
+ let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testTrue") });
+ is(renderedComponent.textContent, "true", "Number rep has expected text content for boolean true");
+
+ renderedComponent = renderComponent(Number.rep, { object: getGripStub("testFalse") });
+ is(renderedComponent.textContent, "false", "Number rep has expected text content for boolean false");
+ }
+
+ function testNegativeZero() {
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testNegZeroGrip") });
+ is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for negative zero value`);
+
+ let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroGrip") });
+ is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero grip");
+
+ renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroValue") });
+ is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero value");
+ }
+
+ function testUnsafeInt() {
+ const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testUnsafeInt") });
+ is(renderedComponent.textContent, "900719925474099100", "Number rep has expected text content for a long number");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testInt":
+ return 5;
+
+ case "testTrue":
+ return true;
+
+ case "testFalse":
+ return false;
+
+ case "testNegZeroValue":
+ return -0;
+
+ case "testNegZeroGrip":
+ return {
+ "type": "-0"
+ };
+
+ case "testUnsafeInt":
+ return 900719925474099122;
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
new file mode 100644
index 000000000..eeb4aa325
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
@@ -0,0 +1,54 @@
+<!-- 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 ObjectWithText rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ObjectWithText</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ObjectWithText } = browserRequire("devtools/client/shared/components/reps/object-with-text");
+
+ let gripStub = {
+ "type": "object",
+ "class": "CSSStyleRule",
+ "actor": "server1.conn3.obj273",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "ObjectWithText",
+ "text": ".Shadow"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, ObjectWithText.rep, `Rep correctly selects ${ObjectWithText.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(ObjectWithText.rep, { object: gripStub });
+ is(renderedComponent.textContent, "\".Shadow\"", "ObjectWithText rep has expected text content");
+ } 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/mochitest/test_reps_object-with-url.html b/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html
new file mode 100644
index 000000000..488c28dc2
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-url.html
@@ -0,0 +1,60 @@
+<!-- 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 ObjectWithURL rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - ObjectWithURL</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { ObjectWithURL } = browserRequire("devtools/client/shared/components/reps/object-with-url");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Location",
+ "actor": "server1.conn2.obj272",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 15,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://www.mozilla.org/en-US/"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, ObjectWithURL.rep, `Rep correctly selects ${ObjectWithURL.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(ObjectWithURL.rep, { object: gripStub });
+ ok(renderedComponent.className.includes("objectBox-Location"), "ObjectWithURL rep has expected class name");
+ const innerNode = renderedComponent.querySelector(".objectPropValue");
+ is(innerNode.textContent, "https://www.mozilla.org/en-US/", "ObjectWithURL rep has expected inner HTML structure and text content");
+
+ // @TODO test link once Bug 1245303 has been implemented.
+ } 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/mochitest/test_reps_object.html b/devtools/client/shared/components/test/mochitest/test_reps_object.html
new file mode 100644
index 000000000..c3332361d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object.html
@@ -0,0 +1,225 @@
+<!-- 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 Obj rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Obj</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Obj } = browserRequire("devtools/client/shared/components/reps/object");
+
+ const componentUnderTest = Obj;
+
+ try {
+ yield testBasic();
+
+ // Test property iterator
+ yield testMaxProps();
+ yield testMoreThanMaxProps();
+ yield testUninterestingProps();
+
+ // Test that properties are rendered as expected by PropRep
+ yield testNested();
+
+ // Test that 'more' property doesn't clobber the caption.
+ yield testMoreProp();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testBasic() {
+ const stub = {};
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, Obj.rep, `Rep correctly selects ${Obj.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Object`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testBasic", componentUnderTest, stub);
+ }
+
+ function testMaxProps() {
+ const testName = "testMaxProps";
+
+ const stub = {a: "a", b: "b", c: "c"};
+ const defaultOutput = `Object { a: "a", b: "b", c: "c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMaxProps", componentUnderTest, stub);
+ }
+
+ function testMoreThanMaxProps() {
+ let stub = {};
+ for (let i = 0; i<100; i++) {
+ stub[`p${i}`] = i
+ }
+ const defaultOutput = `Object { p0: 0, p1: 1, p2: 2, 97 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreThanMaxProps", componentUnderTest, stub);
+ }
+
+ function testUninterestingProps() {
+ const stub = {a:undefined, b:undefined, c:"c", d:0};
+ const defaultOutput = `Object { c: "c", d: 0, a: undefined, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testUninterestingProps", componentUnderTest, stub);
+ }
+
+ function testNested() {
+ const stub = {
+ objProp: {
+ id: 1,
+ arr: [2]
+ },
+ strProp: "test string",
+ arrProp: [1]
+ };
+ const defaultOutput = `Object { strProp: "test string", objProp: Object, arrProp: [1] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testNestedObject", componentUnderTest, stub);
+ }
+
+ function testMoreProp() {
+ const stub = {
+ a: undefined,
+ b: 1,
+ 'more': 2,
+ d: 3
+ };
+ const defaultOutput = `Object { b: 1, more: 2, d: 3, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Object`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testMoreProp", componentUnderTest, stub);
+ }});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_promise.html b/devtools/client/shared/components/test/mochitest/test_reps_promise.html
new file mode 100644
index 000000000..31de7136d
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_promise.html
@@ -0,0 +1,333 @@
+<!-- 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 Promise rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Promise</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { PromiseRep } = browserRequire("devtools/client/shared/components/reps/promise");
+
+ const componentUnderTest = PromiseRep;
+
+ try {
+ yield testPending();
+ yield testFulfilledWithNumber();
+ yield testFulfilledWithString();
+ yield testFulfilledWithObject();
+ yield testFulfilledWithArray();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testPending() {
+ // Test object = `new Promise((resolve, reject) => true)`
+ const stub = getGripStub("testPending");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${PromiseRep.rep.displayName} for pending Promise`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "pending" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "pending" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testPending", componentUnderTest, stub);
+ }
+ function testFulfilledWithNumber() {
+ // Test object = `Promise.resolve(42)`
+ const stub = getGripStub("testFulfilledWithNumber");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with a number`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: 42 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithNumber", componentUnderTest, stub);
+ }
+ function testFulfilledWithString() {
+ // Test object = `Promise.resolve("foo")`
+ const stub = getGripStub("testFulfilledWithString");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with a string`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: "foo" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithString", componentUnderTest, stub);
+ }
+
+ function testFulfilledWithObject() {
+ // Test object = `Promise.resolve({foo: "bar", baz: "boo"})`
+ const stub = getGripStub("testFulfilledWithObject");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with an object`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: Object }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithObject", componentUnderTest, stub);
+ }
+
+ function testFulfilledWithArray() {
+ // Test object = `Promise.resolve([1,2,3])`
+ const stub = getGripStub("testFulfilledWithArray");
+
+ // Test that correct rep is chosen.
+ const renderedRep = shallowRenderComponent(Rep, { object: stub });
+ const {displayName} = PromiseRep.rep;
+ is(renderedRep.type, PromiseRep.rep,
+ `Rep correctly selects ${displayName} for Promise fulfilled with an array`);
+
+ // Test rendering
+ const defaultOutput = `Promise { <state>: "fulfilled", <value>: [3] }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Promise { "fulfilled" }`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testFulfilledWithArray", componentUnderTest, stub);
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testPending":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj54",
+ "class": "Promise",
+ "promiseState": {
+ "state": "pending",
+ "creationTimestamp": 1477327760242.5752
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithNumber":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj55",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": 42,
+ "creationTimestamp": 1477327760242.721,
+ "timeToSettle": 0.018497000000479602
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithString":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj56",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": "foo",
+ "creationTimestamp": 1477327760243.2483,
+ "timeToSettle": 0.0019969999998465937
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithObject":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj59",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj60",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 2
+ },
+ "creationTimestamp": 1477327760243.2214,
+ "timeToSettle": 0.002035999999861815
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ case "testFulfilledWithArray":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj57",
+ "class": "Promise",
+ "promiseState": {
+ "state": "fulfilled",
+ "value": {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj58",
+ "class": "Array",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 4,
+ "preview": {
+ "kind": "ArrayLike",
+ "length": 3
+ }
+ },
+ "creationTimestamp": 1477327760242.9597,
+ "timeToSettle": 0.006158000000141328
+ },
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+ };
+ }
+ return null;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_regexp.html b/devtools/client/shared/components/test/mochitest/test_reps_regexp.html
new file mode 100644
index 000000000..074948494
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_regexp.html
@@ -0,0 +1,51 @@
+<!-- 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 RegExp rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - RegExp</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { RegExp } = browserRequire("devtools/client/shared/components/reps/regexp");
+
+ let gripStub = {
+ "type": "object",
+ "class": "RegExp",
+ "actor": "server1.conn22.obj39",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1,
+ "displayString": "/ab+c/i"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, RegExp.rep, `Rep correctly selects ${RegExp.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(RegExp.rep, { object: gripStub });
+ is(renderedComponent.textContent, "/ab+c/i", "RegExp rep has expected text content");
+ } 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/mochitest/test_reps_string.html b/devtools/client/shared/components/test/mochitest/test_reps_string.html
new file mode 100644
index 000000000..f9fc9e5b0
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_string.html
@@ -0,0 +1,79 @@
+<!-- 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 String rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - String</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { StringRep } = browserRequire("devtools/client/shared/components/reps/string");
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testMultiline") });
+ is(renderedRep.type, StringRep.rep, `Rep correctly selects ${StringRep.rep.displayName}`);
+
+ // Test rendering
+ yield testMultiline();
+ yield testMultilineOpen();
+ yield testMultilineLimit();
+ yield testUseQuotes();
+ yield testNonPritableCharacters();
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testMultiline() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline") });
+ is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string");
+ }
+
+ function testMultilineLimit() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
+ is(renderedComponent.textContent, "\"aaaaaaaaaa…cccccccc\n\"", "String rep has expected text content for multiline string with specified number of characters");
+ }
+
+ function testMultilineOpen() {
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), member: {open: true} });
+ is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n\"", "String rep has expected text content for multiline string when open");
+ }
+
+ function testUseQuotes(){
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testUseQuotes"), useQuotes: false });
+ is(renderedComponent.textContent, "abc", "String rep was expected to omit quotes");
+ }
+
+ function testNonPritableCharacters(){
+ const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testNonPritableCharacters"), useQuotes: false });
+ is(renderedComponent.textContent, "a\ufffdb", "String rep was expected to omit non printable characters");
+ }
+
+ function getGripStub(name) {
+ switch (name) {
+ case "testMultiline":
+ return "aaaaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbb\ncccccccccccccccc\n";
+ case "testUseQuotes":
+ return "abc";
+ case "testNonPritableCharacters":
+ return "a\x01b";
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html b/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html
new file mode 100644
index 000000000..6f54dee48
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_stylesheet.html
@@ -0,0 +1,54 @@
+<!-- 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 Stylesheet rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - Stylesheet</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { StyleSheet } = browserRequire("devtools/client/shared/components/reps/stylesheet");
+
+ let gripStub = {
+ "type": "object",
+ "class": "CSSStyleSheet",
+ "actor": "server1.conn2.obj1067",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "https://example.com/styles.css"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, StyleSheet.rep, `Rep correctly selects ${StyleSheet.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(StyleSheet.rep, { object: gripStub });
+ is(renderedComponent.textContent, "StyleSheet https://example.com/styles.css", "StyleSheet rep has expected text content");
+ } 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/mochitest/test_reps_symbol.html b/devtools/client/shared/components/test/mochitest/test_reps_symbol.html
new file mode 100644
index 000000000..0112eac0f
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_symbol.html
@@ -0,0 +1,77 @@
+<!-- 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 Symbol rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - String</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+/* import-globals-from head.js */
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { SymbolRep } = browserRequire("devtools/client/shared/components/reps/symbol");
+
+ let gripStubs = new Map();
+ gripStubs.set("testSymbolFoo", {
+ type: "symbol",
+ name: "foo"
+ });
+ gripStubs.set("testSymbolWithoutIdentifier", {
+ type: "symbol"
+ });
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(
+ Rep,
+ { object: gripStubs.get("testSymbolFoo")}
+ );
+
+ is(renderedRep.type, SymbolRep.rep,
+ `Rep correctly selects ${SymbolRep.rep.displayName}`);
+
+ // Test rendering
+ yield testSymbol();
+ yield testSymbolWithoutIdentifier();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testSymbol() {
+ const renderedComponent = renderComponent(
+ SymbolRep.rep,
+ { object: gripStubs.get("testSymbolFoo") }
+ );
+
+ is(renderedComponent.textContent, "Symbol(foo)",
+ "Symbol rep has expected text content");
+ }
+
+ function testSymbolWithoutIdentifier() {
+ const renderedComponent = renderComponent(
+ SymbolRep.rep,
+ { object: gripStubs.get("testSymbolWithoutIdentifier") }
+ );
+
+ is(renderedComponent.textContent, "Symbol()",
+ "Symbol rep without identifier has expected text content");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_text-node.html b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
new file mode 100644
index 000000000..f64902a63
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_text-node.html
@@ -0,0 +1,115 @@
+<!-- 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 text-node rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - text-node</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { TextNode } = browserRequire("devtools/client/shared/components/reps/text-node");
+
+ let gripStubs = new Map();
+ gripStubs.set("testRendering", {
+ "class": "Text",
+ "actor": "server1.conn1.child1/obj50",
+ "preview": {
+ "textContent": "hello world"
+ }
+ });
+ gripStubs.set("testRenderingWithEOL", {
+ "class": "Text",
+ "actor": "server1.conn1.child1/obj50",
+ "preview": {
+ "textContent": "hello\nworld"
+ }
+ });
+
+ try {
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, {
+ object: gripStubs.get("testRendering")
+ });
+
+ is(renderedRep.type, TextNode.rep,
+ `Rep correctly selects ${TextNode.rep.displayName}`);
+
+ yield testRendering();
+ yield testRenderingWithEOL();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testRendering() {
+ const stub = gripStubs.get("testRendering");
+ const defaultShortOutput = `"hello world"`;
+ const defaultLongOutput = `<TextNode textContent="hello world">;`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRendering", TextNode, stub);
+ }
+
+ function testRenderingWithEOL() {
+ const stub = gripStubs.get("testRenderingWithEOL");
+ const defaultShortOutput = `"hello\nworld"`;
+ const defaultLongOutput = `<TextNode textContent="hello\nworld">;`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultShortOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultLongOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, "testRenderingWithEOL", TextNode, stub);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_reps_undefined.html b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
new file mode 100644
index 000000000..26b3345ac
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_undefined.html
@@ -0,0 +1,47 @@
+<!-- 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 undefined rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - undefined</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Undefined } = browserRequire("devtools/client/shared/components/reps/undefined");
+
+ let gripStub = {
+ "type": "undefined"
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Undefined.rep, `Rep correctly selects ${Undefined.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Undefined.rep, {});
+ is(renderedComponent.className, "objectBox objectBox-undefined", "Undefined rep has expected class names");
+ is(renderedComponent.textContent, "undefined", "Undefined rep has expected text content");
+ } 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/mochitest/test_reps_window.html b/devtools/client/shared/components/test/mochitest/test_reps_window.html
new file mode 100644
index 000000000..55d60e9f5
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_window.html
@@ -0,0 +1,58 @@
+<!-- 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 window rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep tests - window</title>
+ <script type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+ let { Window } = browserRequire("devtools/client/shared/components/reps/window");
+
+ let gripStub = {
+ "type": "object",
+ "class": "Window",
+ "actor": "server1.conn3.obj198",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 887,
+ "preview": {
+ "kind": "ObjectWithURL",
+ "url": "about:newtab"
+ }
+ };
+
+ // Test that correct rep is chosen
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, Window.rep, `Rep correctly selects ${Window.rep.displayName}`);
+
+ // Test rendering
+ const renderedComponent = renderComponent(Window.rep, { object: gripStub });
+ ok(renderedComponent.className.includes("objectBox-Window"), "Window rep has expected class name");
+ const innerNode = renderedComponent.querySelector(".objectPropValue");
+ is(innerNode.textContent, "about:newtab", "Window rep has expected inner HTML structure and text content");
+ } 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/mochitest/test_sidebar_toggle.html b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
new file mode 100644
index 000000000..252f2fbb1
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_sidebar_toggle.html
@@ -0,0 +1,56 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ let SidebarToggle = browserRequire("devtools/client/shared/components/sidebar-toggle.js");
+
+ try {
+ yield 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.indexOf("pane-collapsed") >= 0,
+ "Proper class name is set");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/shared/components/test/mochitest/test_stack-trace.html b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
new file mode 100644
index 000000000..121316cb4
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
@@ -0,0 +1,102 @@
+<!-- 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>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+/* import-globals-from head.js */
+"use strict";
+
+window.onload = function () {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let StackTrace = React.createFactory(
+ browserRequire("devtools/client/shared/components/stack-trace")
+ );
+ ok(StackTrace, "Got the StackTrace factory");
+
+ add_task(function* () {
+ let stacktrace = [
+ {
+ filename: "http://myfile.com/mahscripts.js",
+ lineNumber: 55,
+ columnNumber: 10
+ },
+ {
+ asyncCause: "because",
+ functionName: "loadFunc",
+ filename: "http://myfile.com/loader.js -> http://myfile.com/loadee.js",
+ lineNumber: 10
+ }
+ ];
+
+ let props = {
+ stacktrace,
+ onViewSourceInDebugger: () => {}
+ };
+
+ let trace = ReactDOM.render(StackTrace(props), window.document.body);
+ yield forceRender(trace);
+
+ let traceEl = trace.getDOMNode();
+ ok(traceEl, "Rendered StackTrace has an element");
+
+ // Get the child nodes and filter out the text-only whitespace ones
+ let frameEls = Array.from(traceEl.childNodes)
+ .filter(n => 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: "http://myfile.com/mahscripts.js",
+ file: "http://myfile.com/mahscripts.js",
+ line: 55,
+ column: 10,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://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: "http://myfile.com/loadee.js",
+ file: "http://myfile.com/loadee.js",
+ line: 10,
+ column: null,
+ shouldLink: true,
+ tooltip: "View source in Debugger → http://myfile.com/loadee.js:10",
+ });
+
+ // Check the tabs and newlines in the stack trace textContent
+ let traceText = traceEl.textContent;
+ let traceLines = traceText.split("\n");
+ ok(traceLines.length > 0, "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/mochitest/test_tabs_accessibility.html b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
new file mode 100644
index 000000000..a86082187
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
@@ -0,0 +1,79 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const InspectorTabPanel = React.createFactory(browserRequire("devtools/client/inspector/components/inspector-tab-panel"));
+ const Tabbar = React.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);
+
+ yield addTabWithPanel(0);
+ yield 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"), "panel-0", "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"), "panel-1", "Anchor aria-controls is set correctly");
+
+ yield 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
+ let panel = document.createElement("div");
+ panel.id = `sidebar-panel-${tabId}`;
+ document.body.appendChild(panel);
+
+ return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+ tabs: tabbarReact.state.tabs.concat({
+ id: `sidebar-panel-${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/mochitest/test_tabs_menu.html b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
new file mode 100644
index 000000000..ac8e99289
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
@@ -0,0 +1,81 @@
+<!-- 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 type="application/javascript" 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="resource://devtools/client/themes/variables.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/common.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/light-theme.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabs.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabbar.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/side-panel.css">
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/inspector-tab-panel.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const Tabbar = React.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);
+
+ // Test panel.
+ let TabPanel = React.createFactory(React.createClass({
+ render: function () {
+ return React.DOM.div({}, "content");
+ }
+ }));
+
+ // Create a few panels.
+ yield addTabWithPanel(1);
+ yield addTabWithPanel(2);
+ yield addTabWithPanel(3);
+ yield addTabWithPanel(4);
+ yield 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/mochitest/test_tree_01.html b/devtools/client/shared/components/test/mochitest/test_tree_01.html
new file mode 100644
index 000000000..dfd666348
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_01.html
@@ -0,0 +1,64 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ 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");
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield 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/mochitest/test_tree_02.html b/devtools/client/shared/components/test/mochitest/test_tree_02.html
new file mode 100644
index 000000000..a1fc33a38
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_02.html
@@ -0,0 +1,45 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ TEST_TREE.expanded = new Set("MNO".split(""));
+ yield 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/mochitest/test_tree_03.html b/devtools/client/shared/components/test/mochitest/test_tree_03.html
new file mode 100644
index 000000000..feabc7e0a
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_03.html
@@ -0,0 +1,46 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ autoExpandDepth: 1
+ })), window.document.body);
+
+ 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/mochitest/test_tree_04.html b/devtools/client/shared/components/test/mochitest/test_tree_04.html
new file mode 100644
index 000000000..24948c003
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_04.html
@@ -0,0 +1,128 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.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/tree"));
+
+ const tree = ReactDOM.render(
+ Tree(Object.assign({}, TEST_TREE_INTERFACE, { itemHeight: ITEM_HEIGHT })),
+ window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ yield setState(tree, {
+ height: 3 * ITEM_HEIGHT,
+ scroll: 1 * ITEM_HEIGHT
+ });
+
+ 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");
+
+ yield setState(tree, {
+ height: 2 * ITEM_HEIGHT,
+ scroll: 3 * ITEM_HEIGHT
+ });
+
+ 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)
+ yield 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");
+
+ yield 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/mochitest/test_tree_05.html b/devtools/client/shared/components/test/mochitest/test_tree_05.html
new file mode 100644
index 000000000..76116ab51
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_05.html
@@ -0,0 +1,83 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield setProps(tree, {
+ focused: "G",
+ });
+
+ 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");
+
+ // Click the first tree node
+ Simulate.click(document.querySelector(".tree-node"));
+ yield 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/mochitest/test_tree_06.html b/devtools/client/shared/components/test/mochitest/test_tree_06.html
new file mode 100644
index 000000000..1d8f28ec9
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_06.html
@@ -0,0 +1,320 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ // UP ----------------------------------------------------------------------
+
+ info("Up to the previous sibling.");
+
+ yield setProps(tree, {
+ focused: "L"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield 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" });
+ yield 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.");
+
+ yield setProps(tree, {
+ focused: "A"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowUp" });
+ yield 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 --------------------------------------------------------------------
+
+ yield setProps(tree, {
+ focused: "K"
+ });
+
+ info("Down to next sibling.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield 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" });
+ yield 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.");
+
+ yield setProps(tree, {
+ focused: "O"
+ });
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowDown" });
+ yield 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.");
+
+ yield setProps(tree, {
+ focused: "L"
+ })
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowLeft" });
+ yield 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" });
+ yield 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" });
+ yield 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 to go to next item.");
+
+ Simulate.keyDown(document.querySelector(".tree"), { key: "ArrowRight" });
+ yield 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.
+ let keysWithModifier = [
+ { key: "ArrowDown", altKey: true },
+ { key: "ArrowDown", ctrlKey: true },
+ { key: "ArrowDown", metaKey: true },
+ { key: "ArrowDown", shiftKey: true },
+ ];
+ for (let key of keysWithModifier) {
+ Simulate.keyDown(document.querySelector(".tree"), key);
+ yield 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/mochitest/test_tree_07.html b/devtools/client/shared/components/test/mochitest/test_tree_07.html
new file mode 100644
index 000000000..178ac77e3
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_07.html
@@ -0,0 +1,64 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.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/tree"));
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ yield setProps(tree, {
+ renderItem: (item, depth, focused, arrow) => {
+ return React.DOM.div(
+ {
+ id: item,
+ style: { marginLeft: depth * 16 + "px" }
+ },
+ arrow,
+ item
+ );
+ }
+ });
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+ yield forceRender(tree);
+
+ let arrows = document.querySelectorAll(".arrow");
+ for (let a of arrows) {
+ ok(a.classList.contains("open"), "Every arrow should be open.");
+ }
+
+ TEST_TREE.expanded = new Set();
+ yield forceRender(tree);
+
+ arrows = document.querySelectorAll(".arrow");
+ for (let 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/mochitest/test_tree_08.html b/devtools/client/shared/components/test/mochitest/test_tree_08.html
new file mode 100644
index 000000000..d024f37f8
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_08.html
@@ -0,0 +1,51 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+ const tree = ReactDOM.render(Tree(Object.assign({}, TEST_TREE_INTERFACE, {
+ onFocus: x => setProps(tree, { focused: x }),
+ })), window.document.body);
+
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+
+ input.focus();
+ is(document.activeElement, input, "The text input should be focused.");
+
+ Simulate.click(document.querySelector(".tree-node"));
+ yield 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/mochitest/test_tree_09.html b/devtools/client/shared/components/test/mochitest/test_tree_09.html
new file mode 100644
index 000000000..96650134b
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_09.html
@@ -0,0 +1,77 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ let numberOfExpands = 0;
+ let lastExpandedItem = null;
+
+ let numberOfCollapses = 0;
+ let lastCollapsedItem = null;
+
+ const tree = ReactDOM.render(Tree(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 => setProps(tree, { focused: item }),
+ })), window.document.body);
+
+ yield setProps(tree, {
+ 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" });
+ yield 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" });
+ yield 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/mochitest/test_tree_10.html b/devtools/client/shared/components/test/mochitest/test_tree_10.html
new file mode 100644
index 000000000..34f8a2633
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_10.html
@@ -0,0 +1,52 @@
+<!-- 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 type="application/javascript" 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;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ const tree = ReactDOM.render(Tree(Object.assign({
+ autoExpandDepth: 1
+ }, TEST_TREE_INTERFACE)), window.document.body);
+
+ yield setProps(tree, {
+ 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/mochitest/test_tree_11.html b/devtools/client/shared/components/test/mochitest/test_tree_11.html
new file mode 100644
index 000000000..1611f7d26
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tree_11.html
@@ -0,0 +1,92 @@
+<!-- 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 type="application/javascript" 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>
+ * {
+ margin: 0;
+ padding: 0;
+ height: 30px;
+ max-height: 30px;
+ min-height: 30px;
+ font-size: 10px;
+ overflow: auto;
+ }
+ </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const { Simulate } = React.addons.TestUtils;
+ const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
+
+ TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
+
+ const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
+
+ yield setProps(tree, {
+ itemHeight: 10,
+ onFocus: item => setProps(tree, { focused: item }),
+ focused: "K",
+ });
+ yield setState(tree, {
+ scroll: 10,
+ });
+ yield forceRender(tree);
+
+ isRenderedTree(document.body.textContent, [
+ "A:false",
+ "-B:false",
+ "--E:false",
+ "---K:true",
+ "---L:false",
+ ], "Should render initial correctly");
+
+ yield 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");
+ yield 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/tree.js b/devtools/client/shared/components/tree.js
new file mode 100644
index 000000000..49b5d1497
--- /dev/null
+++ b/devtools/client/shared/components/tree.js
@@ -0,0 +1,773 @@
+/* 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-env browser */
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+
+const AUTO_EXPAND_DEPTH = 0;
+const NUMBER_OF_OFFSCREEN_ITEMS = 1;
+
+/**
+ * A fast, generic, expandable and collapsible tree component.
+ *
+ * This tree component is fast: it can handle trees with *many* items. It only
+ * renders the subset of those items which are visible in the viewport. It's
+ * been battle tested on huge trees in the memory panel. We've optimized tree
+ * traversal and rendering, even in the presence of cross-compartment wrappers.
+ *
+ * This tree component doesn't make any assumptions about the structure of your
+ * tree data. Whether children are computed on demand, or stored in an array in
+ * the parent's `_children` property, it doesn't matter. We only require the
+ * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded`
+ * functions.
+ *
+ * This tree component is well tested and reliable. See
+ * devtools/client/shared/components/test/mochitest/test_tree_* and its usage in
+ * the performance and memory panels.
+ *
+ * This tree component doesn't make any assumptions about how to render items in
+ * the tree. You provide a `renderItem` function, and this component will ensure
+ * that only those items whose parents are expanded and which are visible in the
+ * viewport are rendered. The `renderItem` function could render the items as a
+ * "traditional" tree or as rows in a table or anything else. It doesn't
+ * restrict you to only one certain kind of tree.
+ *
+ * The only requirement is that every item in the tree render as the same
+ * height. This is required in order to compute which items are visible in the
+ * viewport in constant time.
+ *
+ * ### Example Usage
+ *
+ * Suppose we have some tree data where each item has this form:
+ *
+ * {
+ * id: Number,
+ * label: String,
+ * parent: Item or null,
+ * children: Array of child items,
+ * expanded: bool,
+ * }
+ *
+ * Here is how we could render that data with this component:
+ *
+ * const MyTree = createClass({
+ * displayName: "MyTree",
+ *
+ * propTypes: {
+ * // The root item of the tree, with the form described above.
+ * root: PropTypes.object.isRequired
+ * },
+ *
+ * render() {
+ * return Tree({
+ * itemHeight: 20, // px
+ *
+ * getRoots: () => [this.props.root],
+ *
+ * getParent: item => item.parent,
+ * getChildren: item => item.children,
+ * getKey: item => item.id,
+ * isExpanded: item => item.expanded,
+ *
+ * renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ * let className = "my-tree-item";
+ * if (isFocused) {
+ * className += " focused";
+ * }
+ * return dom.div(
+ * {
+ * className,
+ * // Apply 10px nesting per expansion depth.
+ * style: { marginLeft: depth * 10 + "px" }
+ * },
+ * // Here is the expando arrow so users can toggle expansion and
+ * // collapse state.
+ * arrow,
+ * // And here is the label for this item.
+ * dom.span({ className: "my-tree-item-label" }, item.label)
+ * );
+ * },
+ *
+ * onExpand: item => dispatchExpandActionToRedux(item),
+ * onCollapse: item => dispatchCollapseActionToRedux(item),
+ * });
+ * }
+ * });
+ */
+module.exports = createClass({
+ displayName: "Tree",
+
+ propTypes: {
+ // Required props
+
+ // A function to get an item's parent, or null if it is a root.
+ //
+ // Type: getParent(item: Item) -> Maybe<Item>
+ //
+ // Example:
+ //
+ // // The parent of this item is stored in its `parent` property.
+ // getParent: item => item.parent
+ getParent: PropTypes.func.isRequired,
+
+ // A function to get an item's children.
+ //
+ // Type: getChildren(item: Item) -> [Item]
+ //
+ // Example:
+ //
+ // // This item's children are stored in its `children` property.
+ // getChildren: item => item.children
+ getChildren: PropTypes.func.isRequired,
+
+ // A function which takes an item and ArrowExpander component instance and
+ // returns a component, or text, or anything else that React considers
+ // renderable.
+ //
+ // Type: renderItem(item: Item,
+ // depth: Number,
+ // isFocused: Boolean,
+ // arrow: ReactComponent,
+ // isExpanded: Boolean) -> ReactRenderable
+ //
+ // Example:
+ //
+ // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ // let className = "my-tree-item";
+ // if (isFocused) {
+ // className += " focused";
+ // }
+ // return dom.div(
+ // {
+ // className,
+ // style: { marginLeft: depth * 10 + "px" }
+ // },
+ // arrow,
+ // dom.span({ className: "my-tree-item-label" }, item.label)
+ // );
+ // },
+ renderItem: PropTypes.func.isRequired,
+
+ // A function which returns the roots of the tree (forest).
+ //
+ // Type: getRoots() -> [Item]
+ //
+ // Example:
+ //
+ // // In this case, we only have one top level, root item. You could
+ // // return multiple items if you have many top level items in your
+ // // tree.
+ // getRoots: () => [this.props.rootOfMyTree]
+ getRoots: PropTypes.func.isRequired,
+
+ // A function to get a unique key for the given item. This helps speed up
+ // React's rendering a *TON*.
+ //
+ // Type: getKey(item: Item) -> String
+ //
+ // Example:
+ //
+ // getKey: item => `my-tree-item-${item.uniqueId}`
+ getKey: PropTypes.func.isRequired,
+
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ //
+ // Type: isExpanded(item: Item) -> Boolean
+ //
+ // Example:
+ //
+ // isExpanded: item => item.expanded,
+ isExpanded: PropTypes.func.isRequired,
+
+ // The height of an item in the tree including margin and padding, in
+ // pixels.
+ itemHeight: PropTypes.number.isRequired,
+
+ // Optional props
+
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
+
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
+
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
+
+ // Optional event handlers for when items are expanded or collapsed. Useful
+ // for dispatching redux events and updating application state, maybe lazily
+ // loading subtrees from a worker, etc.
+ //
+ // Type:
+ // onExpand(item: Item)
+ // onCollapse(item: Item)
+ //
+ // Example:
+ //
+ // onExpand: item => dispatchExpandActionToRedux(item)
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ },
+
+ getDefaultProps() {
+ return {
+ autoExpandDepth: AUTO_EXPAND_DEPTH,
+ };
+ },
+
+ getInitialState() {
+ return {
+ scroll: 0,
+ height: window.innerHeight,
+ seen: new Set(),
+ };
+ },
+
+ componentDidMount() {
+ window.addEventListener("resize", this._updateHeight);
+ this._autoExpand();
+ this._updateHeight();
+ },
+
+ componentWillReceiveProps(nextProps) {
+ this._autoExpand();
+ this._updateHeight();
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this._updateHeight);
+ },
+
+ _autoExpand() {
+ if (!this.props.autoExpandDepth) {
+ return;
+ }
+
+ // Automatically expand the first autoExpandDepth levels for new items. Do
+ // not use the usual DFS infrastructure because we don't want to ignore
+ // collapsed nodes.
+ const autoExpand = (item, currentDepth) => {
+ if (currentDepth >= this.props.autoExpandDepth ||
+ this.state.seen.has(item)) {
+ return;
+ }
+
+ this.props.onExpand(item);
+ this.state.seen.add(item);
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(children[i], currentDepth + 1);
+ }
+ };
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ autoExpand(roots[i], 0);
+ }
+ },
+
+ _preventArrowKeyScrolling(e) {
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowDown":
+ case "ArrowLeft":
+ case "ArrowRight":
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.nativeEvent) {
+ if (e.nativeEvent.preventDefault) {
+ e.nativeEvent.preventDefault();
+ }
+ if (e.nativeEvent.stopPropagation) {
+ e.nativeEvent.stopPropagation();
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates the state's height based on clientHeight.
+ */
+ _updateHeight() {
+ this.setState({
+ height: this.refs.tree.clientHeight
+ });
+ },
+
+ /**
+ * Perform a pre-order depth-first search from item.
+ */
+ _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
+ traversal.push({ item, depth: _depth });
+
+ if (!this.props.isExpanded(item)) {
+ return traversal;
+ }
+
+ const nextDepth = _depth + 1;
+
+ if (nextDepth > maxDepth) {
+ return traversal;
+ }
+
+ const children = this.props.getChildren(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(children[i], maxDepth, traversal, nextDepth);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Perform a pre-order depth-first search over the whole forest.
+ */
+ _dfsFromRoots(maxDepth = Infinity) {
+ const traversal = [];
+
+ const roots = this.props.getRoots();
+ const length = roots.length;
+ for (let i = 0; i < length; i++) {
+ this._dfs(roots[i], maxDepth, traversal);
+ }
+
+ return traversal;
+ },
+
+ /**
+ * Expands current row.
+ *
+ * @param {Object} item
+ * @param {Boolean} expandAllChildren
+ */
+ _onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
+ if (this.props.onExpand) {
+ this.props.onExpand(item);
+
+ if (expandAllChildren) {
+ const children = this._dfs(item);
+ const length = children.length;
+ for (let i = 0; i < length; i++) {
+ this.props.onExpand(children[i].item);
+ }
+ }
+ }
+ }),
+
+ /**
+ * Collapses current row.
+ *
+ * @param {Object} item
+ */
+ _onCollapse: oncePerAnimationFrame(function (item) {
+ if (this.props.onCollapse) {
+ this.props.onCollapse(item);
+ }
+ }),
+
+ /**
+ * Sets the passed in item to be the focused item.
+ *
+ * @param {Number} index
+ * The index of the item in a full DFS traversal (ignoring collapsed
+ * nodes). Ignored if `item` is undefined.
+ *
+ * @param {Object|undefined} item
+ * The item to be focused, or undefined to focus no item.
+ */
+ _focus(index, item) {
+ if (item !== undefined) {
+ const itemStartPosition = index * this.props.itemHeight;
+ const itemEndPosition = (index + 1) * this.props.itemHeight;
+
+ // Note that if the height of the viewport (this.state.height) is less
+ // than `this.props.itemHeight`, we could accidentally try and scroll both
+ // up and down in a futile attempt to make both the item's start and end
+ // positions visible. Instead, give priority to the start of the item by
+ // checking its position first, and then using an "else if", rather than
+ // a separate "if", for the end position.
+ if (this.state.scroll > itemStartPosition) {
+ this.refs.tree.scrollTo(0, itemStartPosition);
+ } else if ((this.state.scroll + this.state.height) < itemEndPosition) {
+ this.refs.tree.scrollTo(0, itemEndPosition - this.state.height);
+ }
+ }
+
+ if (this.props.onFocus) {
+ this.props.onFocus(item);
+ }
+ },
+
+ /**
+ * Sets the state to have no focused item.
+ */
+ _onBlur() {
+ this._focus(0, undefined);
+ },
+
+ /**
+ * Fired on a scroll within the tree's container, updates
+ * the stored position of the view port to handle virtual view rendering.
+ *
+ * @param {Event} e
+ */
+ _onScroll: oncePerAnimationFrame(function (e) {
+ this.setState({
+ scroll: Math.max(this.refs.tree.scrollTop, 0),
+ height: this.refs.tree.clientHeight
+ });
+ }),
+
+ /**
+ * Handles key down events in the tree's container.
+ *
+ * @param {Event} e
+ */
+ _onKeyDown(e) {
+ if (this.props.focused == null) {
+ return;
+ }
+
+ // Allow parent nodes to use navigation arrows with modifiers.
+ if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+ return;
+ }
+
+ this._preventArrowKeyScrolling(e);
+
+ switch (e.key) {
+ case "ArrowUp":
+ this._focusPrevNode();
+ return;
+
+ case "ArrowDown":
+ this._focusNextNode();
+ return;
+
+ case "ArrowLeft":
+ if (this.props.isExpanded(this.props.focused)
+ && this.props.getChildren(this.props.focused).length) {
+ this._onCollapse(this.props.focused);
+ } else {
+ this._focusParentNode();
+ }
+ return;
+
+ case "ArrowRight":
+ if (!this.props.isExpanded(this.props.focused)) {
+ this._onExpand(this.props.focused);
+ } else {
+ this._focusNextNode();
+ }
+ return;
+ }
+ },
+
+ /**
+ * Sets the previous node relative to the currently focused item, to focused.
+ */
+ _focusPrevNode: oncePerAnimationFrame(function () {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the previous node in the DFS, if it exists. If it
+ // doesn't exist, we're at the first node already.
+
+ let prev;
+ let prevIndex;
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ for (let i = 0; i < length; i++) {
+ const item = traversal[i].item;
+ if (item === this.props.focused) {
+ break;
+ }
+ prev = item;
+ prevIndex = i;
+ }
+
+ if (prev === undefined) {
+ return;
+ }
+
+ this._focus(prevIndex, prev);
+ }),
+
+ /**
+ * Handles the down arrow key which will focus either the next child
+ * or sibling row.
+ */
+ _focusNextNode: oncePerAnimationFrame(function () {
+ // Start a depth first search and keep going until we reach the currently
+ // focused node. Focus the next node in the DFS, if it exists. If it
+ // doesn't exist, we're at the last node already.
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let i = 0;
+
+ while (i < length) {
+ if (traversal[i].item === this.props.focused) {
+ break;
+ }
+ i++;
+ }
+
+ if (i + 1 < traversal.length) {
+ this._focus(i + 1, traversal[i + 1].item);
+ }
+ }),
+
+ /**
+ * Handles the left arrow key, going back up to the current rows'
+ * parent row.
+ */
+ _focusParentNode: oncePerAnimationFrame(function () {
+ const parent = this.props.getParent(this.props.focused);
+ if (!parent) {
+ return;
+ }
+
+ const traversal = this._dfsFromRoots();
+ const length = traversal.length;
+ let parentIndex = 0;
+ for (; parentIndex < length; parentIndex++) {
+ if (traversal[parentIndex].item === parent) {
+ break;
+ }
+ }
+
+ this._focus(parentIndex, parent);
+ }),
+
+ render() {
+ const traversal = this._dfsFromRoots();
+
+ // 'begin' and 'end' are the index of the first (at least partially) visible item
+ // and the index after the last (at least partially) visible item, respectively.
+ // `NUMBER_OF_OFFSCREEN_ITEMS` is removed from `begin` and added to `end` so that
+ // the top and bottom of the page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS`
+ // previous and next items respectively, which helps the user to see fewer empty
+ // gaps when scrolling quickly.
+ const { itemHeight } = this.props;
+ const { scroll, height } = this.state;
+ const begin = Math.max(((scroll / itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS, 0);
+ const end = Math.ceil((scroll + height) / itemHeight) + NUMBER_OF_OFFSCREEN_ITEMS;
+ const toRender = traversal.slice(begin, end);
+ const topSpacerHeight = begin * itemHeight;
+ const bottomSpacerHeight = Math.max(traversal.length - end, 0) * itemHeight;
+
+ const nodes = [
+ dom.div({
+ key: "top-spacer",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: topSpacerHeight + "px"
+ }
+ })
+ ];
+
+ for (let i = 0; i < toRender.length; i++) {
+ const index = begin + i;
+ const first = index == 0;
+ const last = index == traversal.length - 1;
+ const { item, depth } = toRender[i];
+ nodes.push(TreeNode({
+ key: this.props.getKey(item),
+ index,
+ first,
+ last,
+ item,
+ depth,
+ renderItem: this.props.renderItem,
+ focused: this.props.focused === item,
+ expanded: this.props.isExpanded(item),
+ hasChildren: !!this.props.getChildren(item).length,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: () => this._focus(begin + i, item),
+ onFocusedNodeUnmount: () => this.refs.tree && this.refs.tree.focus(),
+ }));
+ }
+
+ nodes.push(dom.div({
+ key: "bottom-spacer",
+ style: {
+ padding: 0,
+ margin: 0,
+ height: bottomSpacerHeight + "px"
+ }
+ }));
+
+ return dom.div(
+ {
+ className: "tree",
+ ref: "tree",
+ onKeyDown: this._onKeyDown,
+ onKeyPress: this._preventArrowKeyScrolling,
+ onKeyUp: this._preventArrowKeyScrolling,
+ onScroll: this._onScroll,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+ nodes
+ );
+ }
+});
+
+/**
+ * An arrow that displays whether its node is expanded (â–¼) or collapsed
+ * (â–¶). When its node has no children, it is hidden.
+ */
+const ArrowExpander = createFactory(createClass({
+ displayName: "ArrowExpander",
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.item !== nextProps.item
+ || this.props.visible !== nextProps.visible
+ || this.props.expanded !== nextProps.expanded;
+ },
+
+ render() {
+ const attrs = {
+ className: "arrow theme-twisty",
+ onClick: this.props.expanded
+ ? () => this.props.onCollapse(this.props.item)
+ : e => this.props.onExpand(this.props.item, e.altKey)
+ };
+
+ if (this.props.expanded) {
+ attrs.className += " open";
+ }
+
+ if (!this.props.visible) {
+ attrs.style = {
+ visibility: "hidden"
+ };
+ }
+
+ return dom.div(attrs);
+ }
+}));
+
+const TreeNode = createFactory(createClass({
+ componentDidMount() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.focused) {
+ this.refs.button.focus();
+ }
+ },
+
+ componentWillUnmount() {
+ // If this node is being destroyed and has focus, transfer the focus manually
+ // to the parent tree component. Otherwise, the focus will get lost and keyboard
+ // navigation in the tree will stop working. This is a workaround for a XUL bug.
+ // See bugs 1259228 and 1152441 for details.
+ // DE-XUL: Remove this hack once all usages are only in HTML documents.
+ if (this.props.focused) {
+ this.refs.button.blur();
+ if (this.props.onFocusedNodeUnmount) {
+ this.props.onFocusedNodeUnmount();
+ }
+ }
+ },
+
+ _buttonAttrs: {
+ ref: "button",
+ style: {
+ opacity: 0,
+ width: "0 !important",
+ height: "0 !important",
+ padding: "0 !important",
+ outline: "none",
+ MozAppearance: "none",
+ // XXX: Despite resetting all of the above properties (and margin), the
+ // button still ends up with ~79px width, so we set a large negative
+ // margin to completely hide it.
+ MozMarginStart: "-1000px !important",
+ }
+ },
+
+ render() {
+ const arrow = ArrowExpander({
+ item: this.props.item,
+ expanded: this.props.expanded,
+ visible: this.props.hasChildren,
+ onExpand: this.props.onExpand,
+ onCollapse: this.props.onCollapse,
+ });
+
+ let classList = [ "tree-node", "div" ];
+ if (this.props.index % 2) {
+ classList.push("tree-node-odd");
+ }
+ if (this.props.first) {
+ classList.push("tree-node-first");
+ }
+ if (this.props.last) {
+ classList.push("tree-node-last");
+ }
+
+ return dom.div(
+ {
+ className: classList.join(" "),
+ onFocus: this.props.onFocus,
+ onClick: this.props.onFocus,
+ onBlur: this.props.onBlur,
+ "data-expanded": this.props.expanded ? "" : undefined,
+ "data-depth": this.props.depth,
+ style: {
+ padding: 0,
+ margin: 0
+ }
+ },
+
+ this.props.renderItem(this.props.item,
+ this.props.depth,
+ this.props.focused,
+ arrow,
+ this.props.expanded),
+
+ // XXX: OSX won't focus/blur regular elements even if you set tabindex
+ // unless there is an input/button child.
+ dom.button(this._buttonAttrs)
+ );
+ }
+}));
+
+/**
+ * Create a function that calls the given function `fn` only once per animation
+ * frame.
+ *
+ * @param {Function} fn
+ * @returns {Function}
+ */
+function oncePerAnimationFrame(fn) {
+ let animationId = null;
+ let argsToPass = null;
+ return function (...args) {
+ argsToPass = args;
+ if (animationId !== null) {
+ return;
+ }
+
+ animationId = requestAnimationFrame(() => {
+ fn.call(this, ...argsToPass);
+ animationId = null;
+ argsToPass = null;
+ });
+ };
+}
diff --git a/devtools/client/shared/components/tree/label-cell.js b/devtools/client/shared/components/tree/label-cell.js
new file mode 100644
index 000000000..e14875b4d
--- /dev/null
+++ b/devtools/client/shared/components/tree/label-cell.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { td, span } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * Render the default cell used for toggle buttons
+ */
+ let LabelCell = React.createClass({
+ displayName: "LabelCell",
+
+ // See the TreeView component for details related
+ // to the 'member' object.
+ propTypes: {
+ member: PropTypes.object.isRequired
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let level = member.level || 0;
+
+ // Compute indentation dynamically. The deeper the item is
+ // inside the hierarchy, the bigger is the left padding.
+ let rowStyle = {
+ "paddingInlineStart": (level * 16) + "px",
+ };
+
+ let iconClassList = ["treeIcon"];
+ if (member.hasChildren && member.loading) {
+ iconClassList.push("devtools-throbber");
+ } else if (member.hasChildren) {
+ iconClassList.push("theme-twisty");
+ }
+ if (member.open) {
+ iconClassList.push("open");
+ }
+
+ return (
+ td({
+ className: "treeLabelCell",
+ key: "default",
+ style: rowStyle},
+ span({ className: iconClassList.join(" ") }),
+ span({
+ className: "treeLabel " + member.type + "Label",
+ "data-level": level
+ }, member.name)
+ )
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = LabelCell;
+});
diff --git a/devtools/client/shared/components/tree/moz.build b/devtools/client/shared/components/tree/moz.build
new file mode 100644
index 000000000..a7413f25a
--- /dev/null
+++ b/devtools/client/shared/components/tree/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'label-cell.js',
+ 'object-provider.js',
+ 'tree-cell.js',
+ 'tree-header.js',
+ 'tree-row.js',
+ 'tree-view.css',
+ 'tree-view.js',
+)
diff --git a/devtools/client/shared/components/tree/object-provider.js b/devtools/client/shared/components/tree/object-provider.js
new file mode 100644
index 000000000..58519f81f
--- /dev/null
+++ b/devtools/client/shared/components/tree/object-provider.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ /**
+ * Implementation of the default data provider. A provider is state less
+ * object responsible for transformation data (usually a state) to
+ * a structure that can be directly consumed by the tree-view component.
+ */
+ let ObjectProvider = {
+ getChildren: function (object) {
+ let children = [];
+
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return [];
+ }
+
+ if (typeof (object) == "string") {
+ return [];
+ }
+
+ for (let prop in object) {
+ try {
+ children.push(new ObjectProperty(prop, object[prop]));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return children;
+ },
+
+ hasChildren: function (object) {
+ if (object instanceof ObjectProperty) {
+ object = object.value;
+ }
+
+ if (!object) {
+ return false;
+ }
+
+ if (typeof object == "string") {
+ return false;
+ }
+
+ if (typeof object !== "object") {
+ return false;
+ }
+
+ return Object.keys(object).length > 0;
+ },
+
+ getLabel: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.name : null;
+ },
+
+ getValue: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.value : null;
+ },
+
+ getKey: function (object) {
+ return (object instanceof ObjectProperty) ?
+ object.name : null;
+ },
+
+ getType: function (object) {
+ return (object instanceof ObjectProperty) ?
+ typeof object.value : typeof object;
+ }
+ };
+
+ function ObjectProperty(name, value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ // Exports from this module
+ exports.ObjectProperty = ObjectProperty;
+ exports.ObjectProvider = ObjectProvider;
+});
diff --git a/devtools/client/shared/components/tree/tree-cell.js b/devtools/client/shared/components/tree/tree-cell.js
new file mode 100644
index 000000000..f3c48510f
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-cell.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { td, span } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This template represents a cell in TreeView row. It's rendered
+ * using <td> element (the row is <tr> and the entire tree is <table>).
+ */
+ let TreeCell = React.createClass({
+ displayName: "TreeCell",
+
+ // See TreeView component for detailed property explanation.
+ propTypes: {
+ value: PropTypes.any,
+ decorator: PropTypes.object,
+ id: PropTypes.string.isRequired,
+ member: PropTypes.object.isRequired,
+ renderValue: PropTypes.func.isRequired
+ },
+
+ /**
+ * Optimize cell rendering. Rerender cell content only if
+ * the value or expanded state changes.
+ */
+ shouldComponentUpdate: function (nextProps) {
+ return (this.props.value != nextProps.value) ||
+ (this.props.member.open != nextProps.member.open);
+ },
+
+ getCellClass: function (object, id) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getCellClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getCellClass(object, id);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let type = member.type || "";
+ let id = this.props.id;
+ let value = this.props.value;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <td> element.
+ let classNames = this.getCellClass(member.object, id) || [];
+ classNames.push("treeValueCell");
+ classNames.push(type + "Cell");
+
+ // Render value using a default render function or custom
+ // provided function from props or a decorator.
+ let renderValue = this.props.renderValue || defaultRenderValue;
+ if (decorator && decorator.renderValue) {
+ renderValue = decorator.renderValue(member.object, id) || renderValue;
+ }
+
+ let props = Object.assign({}, this.props, {
+ object: value,
+ });
+
+ // Render me!
+ return (
+ td({ className: classNames.join(" ") },
+ span({}, renderValue(props))
+ )
+ );
+ }
+ });
+
+ // Default value rendering.
+ let defaultRenderValue = props => {
+ return (
+ props.object + ""
+ );
+ };
+
+ // Exports from this module
+ module.exports = TreeCell;
+});
diff --git a/devtools/client/shared/components/tree/tree-header.js b/devtools/client/shared/components/tree/tree-header.js
new file mode 100644
index 000000000..eec5363dd
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-header.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Shortcuts
+ const { thead, tr, td, div } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component is responsible for rendering tree header.
+ * It's based on <thead> element.
+ */
+ let TreeHeader = React.createClass({
+ displayName: "TreeHeader",
+
+ // See also TreeView component for detailed info about properties.
+ propTypes: {
+ // Custom tree decorator
+ decorator: PropTypes.object,
+ // True if the header should be visible
+ header: PropTypes.bool,
+ // Array with column definition
+ columns: PropTypes.array
+ },
+
+ getDefaultProps: function () {
+ return {
+ columns: [{
+ id: "default"
+ }]
+ };
+ },
+
+ getHeaderClass: function (colId) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getHeaderClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getHeaderClass(colId);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let cells = [];
+ let visible = this.props.header;
+
+ // Render the rest of the columns (if any)
+ this.props.columns.forEach(col => {
+ let cellStyle = {
+ "width": col.width ? col.width : "",
+ };
+
+ let classNames = [];
+
+ if (visible) {
+ classNames = this.getHeaderClass(col.id);
+ classNames.push("treeHeaderCell");
+ }
+
+ cells.push(
+ td({
+ className: classNames.join(" "),
+ style: cellStyle,
+ key: col.id},
+ div({ className: visible ? "treeHeaderCellBox" : "" },
+ visible ? col.title : ""
+ )
+ )
+ );
+ });
+
+ return (
+ thead({}, tr({ className: visible ? "treeHeaderRow" : "" },
+ cells
+ ))
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = TreeHeader;
+});
diff --git a/devtools/client/shared/components/tree/tree-row.js b/devtools/client/shared/components/tree/tree-row.js
new file mode 100644
index 000000000..adfb1f3ae
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-row.js
@@ -0,0 +1,184 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+ // Tree
+ const TreeCell = React.createFactory(require("./tree-cell"));
+ const LabelCell = React.createFactory(require("./label-cell"));
+
+ // Shortcuts
+ const { tr } = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This template represents a node in TreeView component. It's rendered
+ * using <tr> element (the entire tree is one big <table>).
+ */
+ let TreeRow = React.createClass({
+ displayName: "TreeRow",
+
+ // See TreeView component for more details about the props and
+ // the 'member' object.
+ propTypes: {
+ member: PropTypes.shape({
+ object: PropTypes.obSject,
+ name: PropTypes.sring,
+ type: PropTypes.string.isRequired,
+ rowClass: PropTypes.string.isRequired,
+ level: PropTypes.number.isRequired,
+ hasChildren: PropTypes.bool,
+ value: PropTypes.any,
+ open: PropTypes.bool.isRequired,
+ path: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
+ }),
+ decorator: PropTypes.object,
+ renderCell: PropTypes.object,
+ renderLabelCell: PropTypes.object,
+ columns: PropTypes.array.isRequired,
+ provider: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired
+ },
+
+ componentWillReceiveProps(nextProps) {
+ // I don't like accessing the underlying DOM elements directly,
+ // but this optimization makes the filtering so damn fast!
+ // The row doesn't have to be re-rendered, all we really need
+ // to do is toggling a class name.
+ // The important part is that DOM elements don't need to be
+ // re-created when they should appear again.
+ if (nextProps.member.hidden != this.props.member.hidden) {
+ let row = ReactDOM.findDOMNode(this);
+ row.classList.toggle("hidden");
+ }
+ },
+
+ /**
+ * Optimize row rendering. If props are the same do not render.
+ * This makes the rendering a lot faster!
+ */
+ shouldComponentUpdate: function (nextProps) {
+ let props = ["name", "open", "value", "loading"];
+ for (let p in props) {
+ if (nextProps.member[props[p]] != this.props.member[props[p]]) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ getRowClass: function (object) {
+ let decorator = this.props.decorator;
+ if (!decorator || !decorator.getRowClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getRowClass(object);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <tr> element.
+ let classNames = this.getRowClass(member.object) || [];
+ classNames.push("treeRow");
+ classNames.push(member.type + "Row");
+
+ if (member.hasChildren) {
+ classNames.push("hasChildren");
+ }
+
+ if (member.open) {
+ classNames.push("opened");
+ }
+
+ if (member.loading) {
+ classNames.push("loading");
+ }
+
+ if (member.hidden) {
+ classNames.push("hidden");
+ }
+
+ // The label column (with toggle buttons) is usually
+ // the first one, but there might be cases (like in
+ // the Memory panel) where the toggling is done
+ // in the last column.
+ let cells = [];
+
+ // Get components for rendering cells.
+ let renderCell = this.props.renderCell || RenderCell;
+ let renderLabelCell = this.props.renderLabelCell || RenderLabelCell;
+ if (decorator && decorator.renderLabelCell) {
+ renderLabelCell = decorator.renderLabelCell(member.object) ||
+ renderLabelCell;
+ }
+
+ // Render a cell for every column.
+ this.props.columns.forEach(col => {
+ let props = Object.assign({}, this.props, {
+ key: col.id,
+ id: col.id,
+ value: this.props.provider.getValue(member.object, col.id)
+ });
+
+ if (decorator && decorator.renderCell) {
+ renderCell = decorator.renderCell(member.object, col.id);
+ }
+
+ let render = (col.id == "default") ? renderLabelCell : renderCell;
+
+ // Some cells don't have to be rendered. This happens when some
+ // other cells span more columns. Note that the label cells contains
+ // toggle buttons and should be usually there unless we are rendering
+ // a simple non-expandable table.
+ if (render) {
+ cells.push(render(props));
+ }
+ });
+
+ // Render tree row
+ return (
+ tr({
+ className: classNames.join(" "),
+ onClick: this.props.onClick},
+ cells
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ let RenderCell = props => {
+ return TreeCell(props);
+ };
+
+ let RenderLabelCell = props => {
+ return LabelCell(props);
+ };
+
+ // Exports from this module
+ module.exports = TreeRow;
+});
diff --git a/devtools/client/shared/components/tree/tree-view.css b/devtools/client/shared/components/tree/tree-view.css
new file mode 100644
index 000000000..850533872
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -0,0 +1,157 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url('resource://devtools/client/shared/components/reps/reps.css');
+
+/******************************************************************************/
+/* TreeView Colors */
+
+:root {
+ --tree-link-color: blue;
+ --tree-header-background: #C8D2DC;
+ --tree-header-sorted-background: #AAC3DC;
+}
+
+/******************************************************************************/
+/* TreeView Table*/
+
+.treeTable .treeLabelCell {
+ padding: 2px 0;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+.treeTable .treeLabelCell::after {
+ content: ":";
+ color: var(--object-color);
+}
+
+.treeTable .treeValueCell {
+ padding: 2px 0;
+ padding-inline-start: 5px;
+ overflow: hidden;
+}
+
+.treeTable .treeLabel {
+ cursor: default;
+ overflow: hidden;
+ padding-inline-start: 4px;
+ white-space: nowrap;
+}
+
+/* No paddding if there is actually no label */
+.treeTable .treeLabel:empty {
+ padding-inline-start: 0;
+}
+
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ cursor: pointer;
+ color: var(--tree-link-color);
+ text-decoration: underline;
+}
+
+/* Filtering */
+.treeTable .treeRow.hidden {
+ display: none;
+}
+
+/******************************************************************************/
+/* Toggle Icon */
+
+.treeTable .treeRow .treeIcon {
+ height: 14px;
+ width: 14px;
+ font-size: 10px; /* Set the size of loading spinner */
+ display: inline-block;
+ vertical-align: bottom;
+ margin-inline-start: 3px;
+ padding-top: 1px;
+}
+
+/* All expanded/collapsed styles need to apply on immediate children
+ since there might be nested trees within a tree. */
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
+ cursor: pointer;
+ background-repeat: no-repeat;
+}
+
+/******************************************************************************/
+/* Header */
+
+.treeTable .treeHeaderRow {
+ height: 18px;
+}
+
+.treeTable .treeHeaderCell {
+ cursor: pointer;
+ -moz-user-select: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 0 !important;
+ background: linear-gradient(
+ rgba(255, 255, 255, 0.05),
+ rgba(0, 0, 0, 0.05)),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%) repeat-x var(--tree-header-background);
+ color: var(--theme-body-color);
+ white-space: nowrap;
+}
+
+.treeTable .treeHeaderCellBox {
+ padding: 2px 0;
+ padding-inline-start: 10px;
+ padding-inline-end: 14px;
+}
+
+.treeTable .treeHeaderRow > .treeHeaderCell:first-child > .treeHeaderCellBox {
+ padding: 0;
+}
+
+.treeTable .treeHeaderSorted {
+ background-color: var(--tree-header-sorted-background);
+}
+
+.treeTable .treeHeaderSorted > .treeHeaderCellBox {
+ background: url(chrome://devtools/skin/images/firebug/arrow-down.svg) no-repeat calc(100% - 4px);
+}
+
+.treeTable .treeHeaderSorted.sortedAscending > .treeHeaderCellBox {
+ background-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
+}
+
+.treeTable .treeHeaderCell:hover:active {
+ background-image: linear-gradient(
+ rgba(0, 0, 0, 0.1),
+ transparent),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-light .treeTable .treeRow:hover,
+.theme-dark .treeTable .treeRow:hover {
+ background-color: var(--theme-selection-background-semitransparent) !important;
+}
+
+.theme-firebug .treeTable .treeRow:hover {
+ background-color: var(--theme-body-background);
+}
+
+.theme-light .treeTable .treeLabel,
+.theme-dark .treeTable .treeLabel {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover,
+.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-firebug .treeTable .treeLabel {
+ color: var(--theme-body-color);
+}
diff --git a/devtools/client/shared/components/tree/tree-view.js b/devtools/client/shared/components/tree/tree-view.js
new file mode 100644
index 000000000..9fae9addb
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.js
@@ -0,0 +1,352 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ // ReactJS
+ const React = require("devtools/client/shared/vendor/react");
+
+ // Reps
+ const { ObjectProvider } = require("./object-provider");
+ const TreeRow = React.createFactory(require("./tree-row"));
+ const TreeHeader = React.createFactory(require("./tree-header"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component represents a tree view with expandable/collapsible nodes.
+ * The tree is rendered using <table> element where every node is represented
+ * by <tr> element. The tree is one big table where nodes (rows) are properly
+ * indented from the left to mimic hierarchical structure of the data.
+ *
+ * The tree can have arbitrary number of columns and so, might be use
+ * as an expandable tree-table UI widget as well. By default, there is
+ * one column for node label and one for node value.
+ *
+ * The tree is maintaining its (presentation) state, which consists
+ * from list of expanded nodes and list of columns.
+ *
+ * Complete data provider interface:
+ * var TreeProvider = {
+ * getChildren: function(object);
+ * hasChildren: function(object);
+ * getLabel: function(object, colId);
+ * getValue: function(object, colId);
+ * getKey: function(object);
+ * getType: function(object);
+ * }
+ *
+ * Complete tree decorator interface:
+ * var TreeDecorator = {
+ * getRowClass: function(object);
+ * getCellClass: function(object, colId);
+ * getHeaderClass: function(colId);
+ * renderValue: function(object, colId);
+ * renderRow: function(object);
+ * renderCelL: function(object, colId);
+ * renderLabelCell: function(object);
+ * }
+ */
+ let TreeView = React.createClass({
+ displayName: "TreeView",
+
+ // The only required property (not set by default) is the input data
+ // object that is used to puputate the tree.
+ propTypes: {
+ // The input data object.
+ object: PropTypes.any,
+ className: PropTypes.string,
+ // Data provider (see also the interface above)
+ provider: PropTypes.shape({
+ getChildren: PropTypes.func,
+ hasChildren: PropTypes.func,
+ getLabel: PropTypes.func,
+ getValue: PropTypes.func,
+ getKey: PropTypes.func,
+ getType: PropTypes.func,
+ }).isRequired,
+ // Tree decorator (see also the interface above)
+ decorator: PropTypes.shape({
+ getRowClass: PropTypes.func,
+ getCellClass: PropTypes.func,
+ getHeaderClass: PropTypes.func,
+ renderValue: PropTypes.func,
+ renderRow: PropTypes.func,
+ renderCelL: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ }),
+ // Custom tree row (node) renderer
+ renderRow: PropTypes.func,
+ // Custom cell renderer
+ renderCell: PropTypes.func,
+ // Custom value renderef
+ renderValue: PropTypes.func,
+ // Custom tree label (including a toggle button) renderer
+ renderLabelCell: PropTypes.func,
+ // Set of expanded nodes
+ expandedNodes: PropTypes.object,
+ // Custom filtering callback
+ onFilter: PropTypes.func,
+ // Custom sorting callback
+ onSort: PropTypes.func,
+ // A header is displayed if set to true
+ header: PropTypes.bool,
+ // Array of columns
+ columns: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ width: PropTypes.string
+ }))
+ },
+
+ getDefaultProps: function () {
+ return {
+ object: null,
+ renderRow: null,
+ provider: ObjectProvider,
+ expandedNodes: new Set(),
+ columns: []
+ };
+ },
+
+ getInitialState: function () {
+ return {
+ expandedNodes: this.props.expandedNodes,
+ columns: ensureDefaultColumn(this.props.columns)
+ };
+ },
+
+ // Node expand/collapse
+
+ toggle: function (nodePath) {
+ let nodes = this.state.expandedNodes;
+ if (this.isExpanded(nodePath)) {
+ nodes.delete(nodePath);
+ } else {
+ nodes.add(nodePath);
+ }
+
+ // Compute new state and update the tree.
+ this.setState(Object.assign({}, this.state, {
+ expandedNodes: nodes
+ }));
+ },
+
+ isExpanded: function (nodePath) {
+ return this.state.expandedNodes.has(nodePath);
+ },
+
+ // Event Handlers
+
+ onClickRow: function (nodePath, event) {
+ event.stopPropagation();
+ this.toggle(nodePath);
+ },
+
+ // Filtering & Sorting
+
+ /**
+ * Filter out nodes that don't correspond to the current filter.
+ * @return {Boolean} true if the node should be visible otherwise false.
+ */
+ onFilter: function (object) {
+ let onFilter = this.props.onFilter;
+ return onFilter ? onFilter(object) : true;
+ },
+
+ onSort: function (parent, children) {
+ let onSort = this.props.onSort;
+ return onSort ? onSort(parent, children) : children;
+ },
+
+ // Members
+
+ /**
+ * Return children node objects (so called 'members') for given
+ * parent object.
+ */
+ getMembers: function (parent, level, path) {
+ // Strings don't have children. Note that 'long' strings are using
+ // the expander icon (+/-) to display the entire original value,
+ // but there are no child items.
+ if (typeof parent == "string") {
+ return [];
+ }
+
+ let provider = this.props.provider;
+ let children = provider.getChildren(parent) || [];
+
+ // If the return value is non-array, the children
+ // are being loaded asynchronously.
+ if (!Array.isArray(children)) {
+ return children;
+ }
+
+ children = this.onSort(parent, children) || children;
+
+ return children.map(child => {
+ let key = provider.getKey(child);
+ let nodePath = path + "/" + key;
+ let type = provider.getType(child);
+ let hasChildren = provider.hasChildren(child);
+
+ // Value with no column specified is used for optimization.
+ // The row is re-rendered only if this value changes.
+ // Value for actual column is get when a cell is rendered.
+ let value = provider.getValue(child);
+
+ if (isLongString(value)) {
+ hasChildren = true;
+ }
+
+ // Return value is a 'member' object containing meta-data about
+ // tree node. It describes node label, value, type, etc.
+ return {
+ // An object associated with this node.
+ object: child,
+ // A label for the child node
+ name: provider.getLabel(child),
+ // Data type of the child node (used for CSS customization)
+ type: type,
+ // Class attribute computed from the type.
+ rowClass: "treeRow-" + type,
+ // Level of the child within the hierarchy (top == 0)
+ level: level,
+ // True if this node has children.
+ hasChildren: hasChildren,
+ // Value associated with this node (as provided by the data provider)
+ value: value,
+ // True if the node is expanded.
+ open: this.isExpanded(nodePath),
+ // Node path
+ path: nodePath,
+ // True if the node is hidden (used for filtering)
+ hidden: !this.onFilter(child)
+ };
+ });
+ },
+
+ /**
+ * Render tree rows/nodes.
+ */
+ renderRows: function (parent, level = 0, path = "") {
+ let rows = [];
+ let decorator = this.props.decorator;
+ let renderRow = this.props.renderRow || TreeRow;
+
+ // Get children for given parent node, iterate over them and render
+ // a row for every one. Use row template (a component) from properties.
+ // If the return value is non-array, the children are being loaded
+ // asynchronously.
+ let members = this.getMembers(parent, level, path);
+ if (!Array.isArray(members)) {
+ return members;
+ }
+
+ members.forEach(member => {
+ if (decorator && decorator.renderRow) {
+ renderRow = decorator.renderRow(member.object) || renderRow;
+ }
+
+ let props = Object.assign({}, this.props, {
+ key: member.path,
+ member: member,
+ columns: this.state.columns,
+ onClick: this.onClickRow.bind(this, member.path)
+ });
+
+ // Render single row.
+ rows.push(renderRow(props));
+
+ // If a child node is expanded render its rows too.
+ if (member.hasChildren && member.open) {
+ let childRows = this.renderRows(member.object, level + 1,
+ member.path);
+
+ // If children needs to be asynchronously fetched first,
+ // set 'loading' property to the parent row. Otherwise
+ // just append children rows to the array of all rows.
+ if (!Array.isArray(childRows)) {
+ let lastIndex = rows.length - 1;
+ props.member.loading = true;
+ rows[lastIndex] = React.cloneElement(rows[lastIndex], props);
+ } else {
+ rows = rows.concat(childRows);
+ }
+ }
+ });
+
+ return rows;
+ },
+
+ render: function () {
+ let root = this.props.object;
+ let classNames = ["treeTable"];
+
+ // Use custom class name from props.
+ let className = this.props.className;
+ if (className) {
+ classNames.push(...className.split(" "));
+ }
+
+ // Alright, let's render all tree rows. The tree is one big <table>.
+ let rows = this.renderRows(root, 0, "");
+
+ // This happens when the view needs to do initial asynchronous
+ // fetch for the root object. The tree might provide a hook API
+ // for rendering animated spinner (just like for tree nodes).
+ if (!Array.isArray(rows)) {
+ rows = [];
+ }
+
+ let props = Object.assign({}, this.props, {
+ columns: this.state.columns
+ });
+
+ return (
+ DOM.table({
+ className: classNames.join(" "),
+ cellPadding: 0,
+ cellSpacing: 0},
+ TreeHeader(props),
+ DOM.tbody({},
+ rows
+ )
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ /**
+ * There should always be at least one column (the one with toggle buttons)
+ * and this function ensures that it's true.
+ */
+ function ensureDefaultColumn(columns) {
+ if (!columns) {
+ columns = [];
+ }
+
+ let defaultColumn = columns.filter(col => col.id == "default");
+ if (defaultColumn.length) {
+ return columns;
+ }
+
+ // The default column is usually the first one.
+ return [{id: "default"}, ...columns];
+ }
+
+ function isLongString(value) {
+ return typeof value == "string" && value.length > 50;
+ }
+
+ // Exports from this module
+ module.exports = TreeView;
+});