diff options
Diffstat (limited to 'devtools/client/shared/components')
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; +}); |