summaryrefslogtreecommitdiffstats
path: root/devtools/client/dom
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/dom')
-rw-r--r--devtools/client/dom/.eslintrc.js17
-rw-r--r--devtools/client/dom/content/actions/filter.js21
-rw-r--r--devtools/client/dom/content/actions/grips.js54
-rw-r--r--devtools/client/dom/content/actions/moz.build9
-rw-r--r--devtools/client/dom/content/components/dom-tree.js91
-rw-r--r--devtools/client/dom/content/components/main-frame.js63
-rw-r--r--devtools/client/dom/content/components/main-toolbar.js66
-rw-r--r--devtools/client/dom/content/components/moz.build10
-rw-r--r--devtools/client/dom/content/constants.js9
-rw-r--r--devtools/client/dom/content/dom-decorator.js50
-rw-r--r--devtools/client/dom/content/dom-view.css118
-rw-r--r--devtools/client/dom/content/dom-view.js65
-rw-r--r--devtools/client/dom/content/grip-provider.js97
-rw-r--r--devtools/client/dom/content/moz.build19
-rw-r--r--devtools/client/dom/content/reducers/filter.js29
-rw-r--r--devtools/client/dom/content/reducers/grips.js123
-rw-r--r--devtools/client/dom/content/reducers/index.js14
-rw-r--r--devtools/client/dom/content/reducers/moz.build10
-rw-r--r--devtools/client/dom/content/utils.js27
-rw-r--r--devtools/client/dom/dom-panel.js241
-rw-r--r--devtools/client/dom/dom.html21
-rw-r--r--devtools/client/dom/main.js26
-rw-r--r--devtools/client/dom/moz.build14
-rw-r--r--devtools/client/dom/test/.eslintrc.js6
-rw-r--r--devtools/client/dom/test/browser.ini12
-rw-r--r--devtools/client/dom/test/browser_dom_array.js40
-rw-r--r--devtools/client/dom/test/browser_dom_basic.js24
-rw-r--r--devtools/client/dom/test/browser_dom_refresh.js25
-rw-r--r--devtools/client/dom/test/head.js239
-rw-r--r--devtools/client/dom/test/page_array.html19
-rw-r--r--devtools/client/dom/test/page_basic.html15
31 files changed, 1574 insertions, 0 deletions
diff --git a/devtools/client/dom/.eslintrc.js b/devtools/client/dom/.eslintrc.js
new file mode 100644
index 000000000..1ad36e780
--- /dev/null
+++ b/devtools/client/dom/.eslintrc.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "XMLHttpRequest": true,
+ "window": true,
+ "define": true,
+ "addEventListener": true,
+ "document": true,
+ "dispatchEvent": true,
+ "MessageEvent": true
+ },
+ "rules": {
+ "indent": "off",
+ "padded-blocks": "off",
+ }
+};
diff --git a/devtools/client/dom/content/actions/filter.js b/devtools/client/dom/content/actions/filter.js
new file mode 100644
index 000000000..3fac9d278
--- /dev/null
+++ b/devtools/client/dom/content/actions/filter.js
@@ -0,0 +1,21 @@
+/* -*- 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 constants = require("../constants");
+
+/**
+ * Used to filter DOM panel content.
+ */
+function setVisibilityFilter(filter) {
+ return {
+ filter: filter,
+ type: constants.SET_VISIBILITY_FILTER,
+ };
+}
+
+// Exports from this module
+exports.setVisibilityFilter = setVisibilityFilter;
diff --git a/devtools/client/dom/content/actions/grips.js b/devtools/client/dom/content/actions/grips.js
new file mode 100644
index 000000000..23d4fc895
--- /dev/null
+++ b/devtools/client/dom/content/actions/grips.js
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+ /* globals DomProvider */
+"use strict";
+
+const constants = require("../constants");
+
+/**
+ * Used to fetch grip prototype and properties from the backend.
+ */
+function requestProperties(grip) {
+ return {
+ grip: grip,
+ type: constants.FETCH_PROPERTIES,
+ status: "start",
+ error: false
+ };
+}
+
+/**
+ * Executed when grip properties are received from the backend.
+ */
+function receiveProperties(grip, response, error) {
+ return {
+ grip: grip,
+ type: constants.FETCH_PROPERTIES,
+ status: "end",
+ response: response,
+ error: error
+ };
+}
+
+/**
+ * Used to get properties from the backend and fire an action
+ * when they are received.
+ */
+function fetchProperties(grip) {
+ return dispatch => {
+ // dispatch(requestProperties(grip));
+
+ // Use 'DomProvider' object exposed from the chrome scope.
+ return DomProvider.getPrototypeAndProperties(grip).then(response => {
+ dispatch(receiveProperties(grip, response));
+ });
+ };
+}
+
+// Exports from this module
+exports.requestProperties = requestProperties;
+exports.receiveProperties = receiveProperties;
+exports.fetchProperties = fetchProperties;
diff --git a/devtools/client/dom/content/actions/moz.build b/devtools/client/dom/content/actions/moz.build
new file mode 100644
index 000000000..6454c00cc
--- /dev/null
+++ b/devtools/client/dom/content/actions/moz.build
@@ -0,0 +1,9 @@
+# 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(
+ 'filter.js',
+ 'grips.js',
+)
diff --git a/devtools/client/dom/content/components/dom-tree.js b/devtools/client/dom/content/components/dom-tree.js
new file mode 100644
index 000000000..ef529ac3f
--- /dev/null
+++ b/devtools/client/dom/content/components/dom-tree.js
@@ -0,0 +1,91 @@
+/* -*- 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const { Grip } = require("devtools/client/shared/components/reps/grip");
+
+// DOM Panel
+const { GripProvider } = require("../grip-provider");
+const { DomDecorator } = require("../dom-decorator");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * Renders DOM panel tree.
+ */
+var DomTree = React.createClass({
+ displayName: "DomTree",
+
+ propTypes: {
+ object: PropTypes.any,
+ filter: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ grips: PropTypes.object,
+ },
+
+ /**
+ * Filter DOM properties. Return true if the object
+ * should be visible in the tree.
+ */
+ onFilter: function (object) {
+ if (!this.props.filter) {
+ return true;
+ }
+
+ return (object.name && object.name.indexOf(this.props.filter) > -1);
+ },
+
+ /**
+ * Render DOM panel content
+ */
+ render: function () {
+ let columns = [{
+ "id": "value"
+ }];
+
+ // This is the integration point with Reps. The DomTree is using
+ // Reps to render all values. The code also specifies default rep
+ // used for data types that don't have its own specific template.
+ let renderValue = props => {
+ return Rep(Object.assign({}, props, {
+ defaultRep: Grip,
+ cropLimit: 50,
+ }));
+ };
+
+ return (
+ TreeView({
+ object: this.props.object,
+ provider: new GripProvider(this.props.grips, this.props.dispatch),
+ decorator: new DomDecorator(),
+ mode: "short",
+ columns: columns,
+ renderValue: renderValue,
+ onFilter: this.onFilter
+ })
+ );
+ }
+});
+
+const mapStateToProps = (state) => {
+ return {
+ grips: state.grips,
+ filter: state.filter
+ };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(DomTree);
+
diff --git a/devtools/client/dom/content/components/main-frame.js b/devtools/client/dom/content/components/main-frame.js
new file mode 100644
index 000000000..d786314e2
--- /dev/null
+++ b/devtools/client/dom/content/components/main-frame.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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// DOM Panel
+const DomTree = React.createFactory(require("./dom-tree"));
+const MainToolbar = React.createFactory(require("./main-toolbar"));
+
+// Shortcuts
+const { div } = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * Renders basic layout of the DOM panel. The DOM panel cotent consists
+ * from two main parts: toolbar and tree.
+ */
+var MainFrame = React.createClass({
+ displayName: "MainFrame",
+
+ propTypes: {
+ object: PropTypes.any,
+ filter: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ },
+
+ /**
+ * Render DOM panel content
+ */
+ render: function () {
+ return (
+ div({className: "mainFrame"},
+ MainToolbar({
+ dispatch: this.props.dispatch,
+ object: this.props.object
+ }),
+ div({className: "treeTableBox"},
+ DomTree({
+ object: this.props.object,
+ filter: this.props.filter,
+ })
+ )
+ )
+ );
+ }
+});
+
+// Transform state into props
+// Note: use https://github.com/faassen/reselect for better performance.
+const mapStateToProps = (state) => {
+ return {
+ filter: state.filter
+ };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(MainFrame);
diff --git a/devtools/client/dom/content/components/main-toolbar.js b/devtools/client/dom/content/components/main-toolbar.js
new file mode 100644
index 000000000..c44a6b4ca
--- /dev/null
+++ b/devtools/client/dom/content/components/main-toolbar.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";
+
+// React
+const React = require("devtools/client/shared/vendor/react");
+const { l10n } = require("../utils");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Toolbar, ToolbarButton } = createFactories(require("devtools/client/jsonview/components/reps/toolbar"));
+
+// DOM Panel
+const SearchBox = React.createFactory(require("devtools/client/shared/components/search-box"));
+
+// Actions
+const { fetchProperties } = require("../actions/grips");
+const { setVisibilityFilter } = require("../actions/filter");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * This template is responsible for rendering a toolbar
+ * within the 'Headers' panel.
+ */
+var MainToolbar = React.createClass({
+ displayName: "MainToolbar",
+
+ propTypes: {
+ object: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ },
+
+ onRefresh: function () {
+ this.props.dispatch(fetchProperties(this.props.object));
+ },
+
+ onSearch: function (value) {
+ this.props.dispatch(setVisibilityFilter(value));
+ },
+
+ render: function () {
+ return (
+ Toolbar({},
+ ToolbarButton({
+ className: "btn refresh",
+ onClick: this.onRefresh},
+ l10n.getStr("dom.refresh")
+ ),
+ SearchBox({
+ delay: 250,
+ onChange: this.onSearch,
+ placeholder: l10n.getStr("dom.filterDOMPanel"),
+ type: "filter"
+ })
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = MainToolbar;
diff --git a/devtools/client/dom/content/components/moz.build b/devtools/client/dom/content/components/moz.build
new file mode 100644
index 000000000..0fa1f8089
--- /dev/null
+++ b/devtools/client/dom/content/components/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ 'dom-tree.js',
+ 'main-frame.js',
+ 'main-toolbar.js'
+)
diff --git a/devtools/client/dom/content/constants.js b/devtools/client/dom/content/constants.js
new file mode 100644
index 000000000..f06ca8512
--- /dev/null
+++ b/devtools/client/dom/content/constants.js
@@ -0,0 +1,9 @@
+/* -*- 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";
+
+exports.FETCH_PROPERTIES = "FETCH_PROPERTIES";
+exports.SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
diff --git a/devtools/client/dom/content/dom-decorator.js b/devtools/client/dom/content/dom-decorator.js
new file mode 100644
index 000000000..4042df8d3
--- /dev/null
+++ b/devtools/client/dom/content/dom-decorator.js
@@ -0,0 +1,50 @@
+/* -*- 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 { Property } = require("./reducers/grips");
+
+// Implementation
+
+function DomDecorator() {
+}
+
+/**
+ * Decorator for DOM panel tree component. It's responsible for
+ * appending an icon to read only properties.
+ */
+DomDecorator.prototype = {
+ getRowClass: function (object) {
+ if (object instanceof Property) {
+ let value = object.value;
+ let names = [];
+
+ if (value.enumerable) {
+ names.push("enumerable");
+ }
+ if (value.writable) {
+ names.push("writable");
+ }
+ if (value.configurable) {
+ names.push("configurable");
+ }
+
+ return names;
+ }
+
+ return null;
+ },
+
+ /**
+ * Return custom React template for specified object. The template
+ * might depend on specified column.
+ */
+ getValueRep: function (value, colId) {
+ }
+};
+
+// Exports from this module
+exports.DomDecorator = DomDecorator;
diff --git a/devtools/client/dom/content/dom-view.css b/devtools/client/dom/content/dom-view.css
new file mode 100644
index 000000000..631f8c536
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.css
@@ -0,0 +1,118 @@
+/* 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/. */
+
+/******************************************************************************/
+/* General */
+
+body {
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+}
+
+.mainFrame {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.mainFrame > .treeTableBox {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+/******************************************************************************/
+/* TreeView Customization */
+
+.treeTable {
+ width: 100%;
+}
+
+/* Space for read only properties icon */
+.treeTable td.treeValueCell {
+ padding-inline-start: 16px;
+}
+
+.treeTable .treeLabel,
+.treeTable td.treeValueCell .objectBox {
+ direction: ltr; /* Don't change the direction of english labels */
+}
+
+/* Read only properties have a padlock icon */
+.treeTable tr:not(.writable) td.treeValueCell {
+ background: url("chrome://devtools/skin/images/firebug/read-only.svg") no-repeat;
+ background-position: 1px 5px;
+ background-size: 10px 10px;
+}
+
+.treeTable tr:not(.writable) td.treeValueCell:dir(rtl) {
+ background-position-x: right 1px;
+}
+
+/* Non-enumerable properties are grayed out */
+.treeTable tr:not(.enumerable) td.treeValueCell {
+ opacity: 0.7;
+}
+
+.treeTable > tbody > tr > td {
+ border-bottom: 1px solid #EFEFEF;
+}
+
+/* Label Types */
+.treeTable .userLabel,
+.treeTable .userClassLabel,
+.treeTable .userFunctionLabel {
+ font-weight: bold;
+}
+
+.treeTable .userLabel {
+ color: #000000;
+}
+
+.treeTable .userClassLabel {
+ color: #E90000;
+}
+
+.treeTable .userFunctionLabel {
+ color: #025E2A;
+}
+
+.treeTable .domLabel {
+ color: #000000;
+}
+
+.treeTable .domClassLabel {
+ color: #E90000;
+}
+
+.treeTable .domFunctionLabel {
+ color: #025E2A;
+}
+
+.treeTable .ordinalLabel {
+ color: SlateBlue;
+ font-weight: bold;
+}
+
+/******************************************************************************/
+/* Search box */
+.devtools-searchbox {
+ float: right;
+}
+
+.devtools-searchbox:dir(rtl) {
+ float: left;
+}
+
+/******************************************************************************/
+/* Theme Dark */
+
+.theme-dark .treeTable > tbody > tr > td {
+ border-bottom: none;
+}
+
+.theme-dark body {
+ background-color: var(--theme-body-background);
+}
diff --git a/devtools/client/dom/content/dom-view.js b/devtools/client/dom/content/dom-view.js
new file mode 100644
index 000000000..b0ea11dee
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.js
@@ -0,0 +1,65 @@
+/* -*- 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+
+// DOM Panel
+const MainFrame = React.createFactory(require("./components/main-frame"));
+
+// Store
+const createStore = require("devtools/client/shared/redux/create-store")({
+ log: false
+});
+
+const { reducers } = require("./reducers/index");
+const store = createStore(combineReducers(reducers));
+
+/**
+ * This object represents view of the DOM panel and is responsible
+ * for rendering the content. It renders the top level ReactJS
+ * component: the MainFrame.
+ */
+function DomView(localStore) {
+ addEventListener("devtools/chrome/message",
+ this.onMessage.bind(this), true);
+
+ // Make it local so, tests can access it.
+ this.store = localStore;
+}
+
+DomView.prototype = {
+ initialize: function (rootGrip) {
+ let content = document.querySelector("#content");
+ let mainFrame = MainFrame({
+ object: rootGrip,
+ });
+
+ // Render top level component
+ let provider = React.createElement(Provider, {
+ store: this.store
+ }, mainFrame);
+
+ this.mainFrame = ReactDOM.render(provider, content);
+ },
+
+ onMessage: function (event) {
+ let data = event.data;
+ let method = data.type;
+
+ if (typeof this[method] == "function") {
+ this[method](data.args);
+ }
+ },
+};
+
+// Construct DOM panel view object and expose it to tests.
+// Tests can access it throught: |panel.panelWin.view|
+window.view = new DomView(store);
diff --git a/devtools/client/dom/content/grip-provider.js b/devtools/client/dom/content/grip-provider.js
new file mode 100644
index 000000000..bcda1ff18
--- /dev/null
+++ b/devtools/client/dom/content/grip-provider.js
@@ -0,0 +1,97 @@
+/* -*- 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 { fetchProperties } = require("./actions/grips");
+const { Property } = require("./reducers/grips");
+
+// Implementation
+function GripProvider(grips, dispatch) {
+ this.grips = grips;
+ this.dispatch = dispatch;
+}
+
+/**
+ * This object provides data for the tree displayed in the tooltip
+ * content.
+ */
+GripProvider.prototype = {
+ /**
+ * Fetches properties from the backend. These properties might be
+ * displayed as child objects in e.g. a tree UI widget.
+ */
+ getChildren: function (object) {
+ let grip = object;
+ if (object instanceof Property) {
+ grip = this.getValue(object);
+ }
+
+ if (!grip || !grip.actor) {
+ return [];
+ }
+
+ let props = this.grips.get(grip.actor);
+ if (!props) {
+ // Fetch missing data from the backend. Returning a promise
+ // from data provider causes the tree to show a spinner.
+ return this.dispatch(fetchProperties(grip));
+ }
+
+ return props;
+ },
+
+ hasChildren: function (object) {
+ if (object instanceof Property) {
+ let value = this.getValue(object);
+ if (!value) {
+ return false;
+ }
+
+ let hasChildren = value.ownPropertyLength > 0;
+
+ if (value.preview) {
+ hasChildren = hasChildren || value.preview.ownPropertiesLength > 0;
+ }
+
+ if (value.preview) {
+ let preview = value.preview;
+ let k = preview.kind;
+ let objectsWithProps = ["DOMNode", "ObjectWithURL"];
+ hasChildren = hasChildren || (objectsWithProps.indexOf(k) != -1);
+ hasChildren = hasChildren || (k == "ArrayLike" && preview.length > 0);
+ }
+
+ return (value.type == "object" && hasChildren);
+ }
+
+ return null;
+ },
+
+ getValue: function (object) {
+ if (object instanceof Property) {
+ let value = object.value;
+ return (typeof value.value != "undefined") ? value.value :
+ value.getterValue;
+ }
+
+ return object;
+ },
+
+ getLabel: function (object) {
+ return (object instanceof Property) ? object.name : null;
+ },
+
+ getKey: function (object) {
+ return (object instanceof Property) ? object.key : null;
+ },
+
+ getType: function (object) {
+ return object.class ? object.class : "";
+ },
+};
+
+// Exports from this module
+exports.GripProvider = GripProvider;
diff --git a/devtools/client/dom/content/moz.build b/devtools/client/dom/content/moz.build
new file mode 100644
index 000000000..b4a9c76bf
--- /dev/null
+++ b/devtools/client/dom/content/moz.build
@@ -0,0 +1,19 @@
+# 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 += [
+ 'actions',
+ 'components',
+ 'reducers',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'dom-decorator.js',
+ 'dom-view.css',
+ 'dom-view.js',
+ 'grip-provider.js',
+ 'utils.js',
+)
diff --git a/devtools/client/dom/content/reducers/filter.js b/devtools/client/dom/content/reducers/filter.js
new file mode 100644
index 000000000..3eb5bd3fc
--- /dev/null
+++ b/devtools/client/dom/content/reducers/filter.js
@@ -0,0 +1,29 @@
+/* -*- 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 constants = require("../constants");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return "";
+}
+
+/**
+ * Filter displayed object properties.
+ */
+function filter(state = getInitialState(), action) {
+ if (action.type == constants.SET_VISIBILITY_FILTER) {
+ return action.filter;
+ }
+
+ return state;
+}
+
+// Exports from this module
+exports.filter = filter;
diff --git a/devtools/client/dom/content/reducers/grips.js b/devtools/client/dom/content/reducers/grips.js
new file mode 100644
index 000000000..c7d589434
--- /dev/null
+++ b/devtools/client/dom/content/reducers/grips.js
@@ -0,0 +1,123 @@
+/* -*- 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 constants = require("../constants");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return new Map();
+}
+
+/**
+ * Maintain a cache of received grip responses from the backend.
+ */
+function grips(state = getInitialState(), action) {
+ // This reducer supports only one action, fetching actor properties
+ // from the backend so, bail out if we are dealing with any other
+ // action.
+ if (action.type != constants.FETCH_PROPERTIES) {
+ return state;
+ }
+
+ switch (action.status) {
+ case "start":
+ return onRequestProperties(state, action);
+ case "end":
+ return onReceiveProperties(state, action);
+ }
+
+ return state;
+}
+
+/**
+ * Handle requestProperties action
+ */
+function onRequestProperties(state, action) {
+ return state;
+}
+
+/**
+ * Handle receiveProperties action
+ */
+function onReceiveProperties(cache, action) {
+ let response = action.response;
+ let from = response.from;
+ let className = action.grip.class;
+
+ // Properly deal with getters.
+ mergeProperties(response);
+
+ // Compute list of requested children.
+ let previewProps = response.preview ? response.preview.ownProperties : null;
+ let ownProps = response.ownProperties || previewProps || [];
+
+ let props = Object.keys(ownProps).map(key => {
+ // Array indexes as a special case. We convert any keys that are string
+ // representations of integers to integers.
+ if (className === "Array" && isInteger(key)) {
+ key = parseInt(key, 10);
+ }
+ return new Property(key, ownProps[key], key);
+ });
+
+ props.sort(sortName);
+
+ // Return new state/map.
+ let newCache = new Map(cache);
+ newCache.set(from, props);
+
+ return newCache;
+}
+
+// Helpers
+
+function mergeProperties(response) {
+ let { ownProperties } = response;
+
+ // 'safeGetterValues' is new and isn't necessary defined on old grips.
+ let safeGetterValues = response.safeGetterValues || {};
+
+ // Merge the safe getter values into one object such that we can use it
+ // in variablesView.
+ for (let name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+ ownProperties[name].getterValue = getterValue;
+ ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+}
+
+function sortName(a, b) {
+ // Display non-enumerable properties at the end.
+ if (!a.value.enumerable && b.value.enumerable) {
+ return 1;
+ }
+ if (a.value.enumerable && !b.value.enumerable) {
+ return -1;
+ }
+ return a.name > b.name ? 1 : -1;
+}
+
+function isInteger(n) {
+ // We use parseInt(n, 10) == n to disregard scientific notation e.g. "3e24"
+ return isFinite(n) && parseInt(n, 10) == n;
+}
+
+function Property(name, value, key) {
+ this.name = name;
+ this.value = value;
+ this.key = key;
+}
+
+// Exports from this module
+exports.grips = grips;
+exports.Property = Property;
diff --git a/devtools/client/dom/content/reducers/index.js b/devtools/client/dom/content/reducers/index.js
new file mode 100644
index 000000000..1900487e1
--- /dev/null
+++ b/devtools/client/dom/content/reducers/index.js
@@ -0,0 +1,14 @@
+/* -*- 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 { grips } = require("./grips");
+const { filter } = require("./filter");
+
+exports.reducers = {
+ grips,
+ filter,
+};
diff --git a/devtools/client/dom/content/reducers/moz.build b/devtools/client/dom/content/reducers/moz.build
new file mode 100644
index 000000000..0a00b3feb
--- /dev/null
+++ b/devtools/client/dom/content/reducers/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ 'filter.js',
+ 'grips.js',
+ 'index.js',
+)
diff --git a/devtools/client/dom/content/utils.js b/devtools/client/dom/content/utils.js
new file mode 100644
index 000000000..645ba7921
--- /dev/null
+++ b/devtools/client/dom/content/utils.js
@@ -0,0 +1,27 @@
+/* -*- 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";
+
+/**
+ * The default localization just returns the last part of the key
+ * (all after the last dot).
+ */
+const DefaultL10N = {
+ getStr: function (key) {
+ let index = key.lastIndexOf(".");
+ return key.substr(index + 1);
+ }
+};
+
+/**
+ * The 'l10n' object is set by main.js in case the DOM panel content
+ * runs within a scope with chrome privileges.
+ *
+ * Note that DOM panel content can also run within a scope with no chrome
+ * privileges, e.g. in an iframe with type 'content' or in a browser tab,
+ * which allows using our own tools for development.
+ */
+exports.l10n = window.l10n || DefaultL10N;
diff --git a/devtools/client/dom/dom-panel.js b/devtools/client/dom/dom-panel.js
new file mode 100644
index 000000000..5cb6d0061
--- /dev/null
+++ b/devtools/client/dom/dom-panel.js
@@ -0,0 +1,241 @@
+/* -*- 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 { Cu } = require("chrome");
+const defer = require("devtools/shared/defer");
+const { ObjectClient } = require("devtools/shared/client/main");
+
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Task } = require("devtools/shared/task");
+
+/**
+ * This object represents DOM panel. It's responsibility is to
+ * render Document Object Model of the current debugger target.
+ */
+function DomPanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this._toolbox = toolbox;
+
+ this.onTabNavigated = this.onTabNavigated.bind(this);
+ this.onContentMessage = this.onContentMessage.bind(this);
+ this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
+
+ this.pendingRequests = new Map();
+
+ EventEmitter.decorate(this);
+}
+
+DomPanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the DOM panel completes opening.
+ */
+ open: Task.async(function* () {
+ if (this._opening) {
+ return this._opening;
+ }
+
+ let deferred = promise.defer();
+ this._opening = deferred.promise;
+
+ // Local monitoring needs to make the target remote.
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ this.initialize();
+
+ this.isReady = true;
+ this.emit("ready");
+ deferred.resolve(this);
+
+ return this._opening;
+ }),
+
+ // Initialization
+
+ initialize: function () {
+ this.panelWin.addEventListener("devtools/content/message",
+ this.onContentMessage, true);
+
+ this.target.on("navigate", this.onTabNavigated);
+ this._toolbox.on("select", this.onPanelVisibilityChange);
+
+ let provider = {
+ getPrototypeAndProperties: this.getPrototypeAndProperties.bind(this)
+ };
+
+ exportIntoContentScope(this.panelWin, provider, "DomProvider");
+
+ this.shouldRefresh = true;
+ },
+
+ destroy: Task.async(function* () {
+ if (this._destroying) {
+ return this._destroying;
+ }
+
+ let deferred = promise.defer();
+ this._destroying = deferred.promise;
+
+ this.target.off("navigate", this.onTabNavigated);
+ this._toolbox.off("select", this.onPanelVisibilityChange);
+
+ this.emit("destroyed");
+
+ deferred.resolve();
+ return this._destroying;
+ }),
+
+ // Events
+
+ refresh: function () {
+ // Do not refresh if the panel isn't visible.
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ // Do not refresh if it isn't necessary.
+ if (!this.shouldRefresh) {
+ return;
+ }
+
+ // Alright reset the flag we are about to refresh the panel.
+ this.shouldRefresh = false;
+
+ this.getRootGrip().then(rootGrip => {
+ this.postContentMessage("initialize", rootGrip);
+ });
+ },
+
+ /**
+ * Make sure the panel is refreshed when the page is reloaded.
+ * The panel is refreshed immediatelly if it's currently selected
+ * or lazily when the user actually selects it.
+ */
+ onTabNavigated: function () {
+ this.shouldRefresh = true;
+ this.refresh();
+ },
+
+ /**
+ * Make sure the panel is refreshed (if needed) when it's selected.
+ */
+ onPanelVisibilityChange: function () {
+ this.refresh();
+ },
+
+ // Helpers
+
+ /**
+ * Return true if the DOM panel is currently selected.
+ */
+ isPanelVisible: function () {
+ return this._toolbox.currentToolId === "dom";
+ },
+
+ getPrototypeAndProperties: function (grip) {
+ let deferred = defer();
+
+ if (!grip.actor) {
+ console.error("No actor!", grip);
+ deferred.reject(new Error("Failed to get actor from grip."));
+ return deferred.promise;
+ }
+
+ // Bail out if target doesn't exist (toolbox maybe closed already).
+ if (!this.target) {
+ return deferred.promise;
+ }
+
+ // If a request for the grips is already in progress
+ // use the same promise.
+ let request = this.pendingRequests.get(grip.actor);
+ if (request) {
+ return request;
+ }
+
+ let client = new ObjectClient(this.target.client, grip);
+ client.getPrototypeAndProperties(response => {
+ this.pendingRequests.delete(grip.actor, deferred.promise);
+ deferred.resolve(response);
+
+ // Fire an event about not having any pending requests.
+ if (!this.pendingRequests.size) {
+ this.emit("no-pending-requests");
+ }
+ });
+
+ this.pendingRequests.set(grip.actor, deferred.promise);
+
+ return deferred.promise;
+ },
+
+ getRootGrip: function () {
+ let deferred = defer();
+
+ // Attach Console. It might involve RDP communication, so wait
+ // asynchronously for the result
+ this.target.activeConsole.evaluateJSAsync("window", res => {
+ deferred.resolve(res.result);
+ });
+
+ return deferred.promise;
+ },
+
+ postContentMessage: function (type, args) {
+ let data = {
+ type: type,
+ args: args,
+ };
+
+ let event = new this.panelWin.MessageEvent("devtools/chrome/message", {
+ bubbles: true,
+ cancelable: true,
+ data: data,
+ });
+
+ this.panelWin.dispatchEvent(event);
+ },
+
+ onContentMessage: function (event) {
+ let data = event.data;
+ let method = data.type;
+ if (typeof this[method] == "function") {
+ this[method](data.args);
+ }
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+};
+
+// Helpers
+
+function exportIntoContentScope(win, obj, defineAs) {
+ let clone = Cu.createObjectIn(win, {
+ defineAs: defineAs
+ });
+
+ let props = Object.getOwnPropertyNames(obj);
+ for (let i = 0; i < props.length; i++) {
+ let propName = props[i];
+ let propValue = obj[propName];
+ if (typeof propValue == "function") {
+ Cu.exportFunction(propValue, clone, {
+ defineAs: propName
+ });
+ }
+ }
+}
+
+// Exports from this module
+exports.DomPanel = DomPanel;
diff --git a/devtools/client/dom/dom.html b/devtools/client/dom/dom.html
new file mode 100644
index 000000000..5fe473d09
--- /dev/null
+++ b/devtools/client/dom/dom.html
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.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 dir="">
+<head>
+ <meta charset="utf-8"/>
+
+ <link href="resource://devtools/client/dom/content/dom-view.css" rel="stylesheet" />
+ <link href="resource://devtools/client/jsonview/css/toolbar.css" rel="stylesheet" />
+ <link href="resource://devtools/client/shared/components/tree/tree-view.css" rel="stylesheet" />
+
+ <script type="text/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"></script>
+</head>
+<body class="theme-body devtools-monospace" role="application">
+ <div id="content"></div>
+ <script type="text/javascript" src="./main.js"></script>
+</body>
+</html>
diff --git a/devtools/client/dom/main.js b/devtools/client/dom/main.js
new file mode 100644
index 000000000..085393428
--- /dev/null
+++ b/devtools/client/dom/main.js
@@ -0,0 +1,26 @@
+/* -*- 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 { utils: Cu } = Components;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+// Module Loader
+const require = BrowserLoader({
+ baseURI: "resource://devtools/client/dom/",
+ window
+}).require;
+
+XPCOMUtils.defineConstant(this, "require", require);
+
+// Localization
+const { LocalizationHelper } = require("devtools/shared/l10n");
+this.l10n = new LocalizationHelper("devtools/client/locales/dom.properties");
+
+// Load DOM panel content
+require("./content/dom-view.js");
diff --git a/devtools/client/dom/moz.build b/devtools/client/dom/moz.build
new file mode 100644
index 000000000..1e04a09dc
--- /dev/null
+++ b/devtools/client/dom/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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DIRS += [
+ 'content',
+]
+
+DevToolsModules(
+ 'dom-panel.js',
+)
diff --git a/devtools/client/dom/test/.eslintrc.js b/devtools/client/dom/test/.eslintrc.js
new file mode 100644
index 000000000..140985533
--- /dev/null
+++ b/devtools/client/dom/test/.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/dom/test/browser.ini b/devtools/client/dom/test/browser.ini
new file mode 100644
index 000000000..e8e35b32b
--- /dev/null
+++ b/devtools/client/dom/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ page_array.html
+ page_basic.html
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_dom_array.js]
+[browser_dom_basic.js]
+[browser_dom_refresh.js]
diff --git a/devtools/client/dom/test/browser_dom_array.js b/devtools/client/dom/test/browser_dom_array.js
new file mode 100644
index 000000000..2813af320
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_array.js
@@ -0,0 +1,40 @@
+/* -*- 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";
+
+const TEST_PAGE_URL = URL_ROOT + "page_array.html";
+const TEST_ARRAY = [
+ "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"
+];
+
+/**
+ * Basic test that checks content of the DOM panel.
+ */
+add_task(function* () {
+ info("Test DOM Panel Array Expansion started");
+
+ let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+ // Expand specified row and wait till children are displayed.
+ yield expandRow(panel, "_a");
+
+ // Verify that children is displayed now.
+ let childRows = getAllRowsForLabel(panel, "_a");
+
+ let item = childRows.pop();
+ is(item.name, "length", "length property is correct");
+ is(item.value, 26, "length property value is 26");
+
+ let i = 0;
+ for (let name in childRows) {
+ let row = childRows[name];
+
+ is(name, i++, `index ${name} is correct and sorted into the correct position`);
+ ok(typeof row.name === "number", "array index is displayed as a number");
+ is(TEST_ARRAY[name], row.value, `value for array[${name}] is ${row.value}`);
+ }
+});
diff --git a/devtools/client/dom/test/browser_dom_basic.js b/devtools/client/dom/test/browser_dom_basic.js
new file mode 100644
index 000000000..2b76fe0fe
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_basic.js
@@ -0,0 +1,24 @@
+/* -*- 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";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+
+/**
+ * Basic test that checks content of the DOM panel.
+ */
+add_task(function* () {
+ info("Test DOM panel basic started");
+
+ let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+ // Expand specified row and wait till children are displayed.
+ yield expandRow(panel, "_a");
+
+ // Verify that child is displayed now.
+ let childRow = getRowByLabel(panel, "_data");
+ ok(childRow, "Child row must exist");
+});
diff --git a/devtools/client/dom/test/browser_dom_refresh.js b/devtools/client/dom/test/browser_dom_refresh.js
new file mode 100644
index 000000000..9fdc6aa7f
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_refresh.js
@@ -0,0 +1,25 @@
+/* -*- 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";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+
+/**
+ * Basic test that checks the Refresh action in DOM panel.
+ */
+add_task(function* () {
+ info("Test DOM panel basic started");
+
+ let { panel } = yield addTestTab(TEST_PAGE_URL);
+
+ // Create a new variable in the page scope and refresh the panel.
+ yield evaluateJSAsync(panel, "var _b = 10");
+ yield refreshPanel(panel);
+
+ // Verify that the variable is displayed now.
+ let row = getRowByLabel(panel, "_b");
+ ok(row, "New variable must be displayed");
+});
diff --git a/devtools/client/dom/test/head.js b/devtools/client/dom/test/head.js
new file mode 100644
index 000000000..f9382786b
--- /dev/null
+++ b/devtools/client/dom/test/head.js
@@ -0,0 +1,239 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+const FRAME_SCRIPT_UTILS_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+// DOM panel actions.
+const constants = require("devtools/client/dom/content/constants");
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dom.enabled", true);
+
+// Enable the DOM panel
+Services.prefs.setBoolPref("devtools.dom.enabled", true);
+
+registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.dom.enabled");
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url
+ * The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when
+ * the url is loaded
+ */
+function addTestTab(url) {
+ info("Adding a new test tab with URL: '" + url + "'");
+
+ return new Promise(resolve => {
+ addTab(url).then(tab => {
+ // Load devtools/shared/frame-script-utils.js
+ getFrameScript();
+
+ // Select the DOM panel and wait till it's initialized.
+ initDOMPanel(tab).then(panel => {
+ waitForDispatch(panel, "FETCH_PROPERTIES").then(() => {
+ resolve({
+ tab: tab,
+ browser: tab.linkedBrowser,
+ panel: panel
+ });
+ });
+ });
+ });
+ });
+}
+
+/**
+ * Open the DOM panel for the given tab.
+ *
+ * @param {nsIDOMElement} tab
+ * Optional tab element for which you want open the DOM panel.
+ * The default tab is taken from the global variable |tab|.
+ * @return a promise that is resolved once the web console is open.
+ */
+function initDOMPanel(tab) {
+ return new Promise(resolve => {
+ let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "dom").then(toolbox => {
+ let panel = toolbox.getCurrentPanel();
+ resolve(panel);
+ });
+ });
+}
+
+/**
+ * Synthesize asynchronous click event (with clean stack trace).
+ */
+function synthesizeMouseClickSoon(panel, element) {
+ return new Promise(resolve => {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(element, 2, 2, {}, panel.panelWin);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Returns tree row with specified label.
+ */
+function getRowByLabel(panel, text) {
+ let doc = panel.panelWin.document;
+ let labels = [...doc.querySelectorAll(".treeLabel")];
+ let label = labels.find(node => node.textContent == text);
+ return label ? label.closest(".treeRow") : null;
+}
+
+/**
+ * Returns the children (tree row text) of the specified object name as an
+ * array.
+ */
+function getAllRowsForLabel(panel, text) {
+ let rootObjectLevel;
+ let node;
+ let result = [];
+ let doc = panel.panelWin.document;
+ let nodes = [...doc.querySelectorAll(".treeLabel")];
+
+ // Find the label (object name) for which we want the children. We remove
+ // nodes from the start of the array until we reach the property. The children
+ // are then at the start of the array.
+ while (true) {
+ node = nodes.shift();
+
+ if (!node || node.textContent === text) {
+ rootObjectLevel = node.getAttribute("data-level");
+ break;
+ }
+ }
+
+ // Return an empty array if the node is not found.
+ if (!node) {
+ return result;
+ }
+
+ // Now get the children.
+ for (node of nodes) {
+ let level = node.getAttribute("data-level");
+
+ if (level > rootObjectLevel) {
+ result.push({
+ name: normalizeTreeValue(node.textContent),
+ value: normalizeTreeValue(node.parentNode.nextElementSibling.textContent)
+ });
+ } else {
+ break;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Strings in the tree are in the form ""a"" and numbers in the form "1". We
+ * normalize these values by converting ""a"" to "a" and "1" to 1.
+ *
+ * @param {String} value
+ * The value to normalize.
+ * @return {String|Number}
+ * The normalized value.
+ */
+function normalizeTreeValue(value) {
+ if (value === `""`) {
+ return "";
+ }
+ if (value.startsWith(`"`) && value.endsWith(`"`)) {
+ return value.substr(1, value.length - 2);
+ }
+ if (isFinite(value) && parseInt(value, 10) == value) {
+ return parseInt(value, 10);
+ }
+
+ return value;
+}
+
+/**
+ * Expands elements with given label and waits till
+ * children are received from the backend.
+ */
+function expandRow(panel, labelText) {
+ let row = getRowByLabel(panel, labelText);
+ return synthesizeMouseClickSoon(panel, row).then(() => {
+ // Wait till children (properties) are fetched
+ // from the backend.
+ return waitForDispatch(panel, "FETCH_PROPERTIES");
+ });
+}
+
+function evaluateJSAsync(panel, expression) {
+ return new Promise(resolve => {
+ panel.target.activeConsole.evaluateJSAsync(expression, res => {
+ resolve(res);
+ });
+ });
+}
+
+function refreshPanel(panel) {
+ let doc = panel.panelWin.document;
+ let button = doc.querySelector(".btn.refresh");
+ return synthesizeMouseClickSoon(panel, button).then(() => {
+ // Wait till children (properties) are fetched
+ // from the backend.
+ return waitForDispatch(panel, "FETCH_PROPERTIES");
+ });
+}
+
+// Redux related API, use from shared location
+// as soon as bug 1261076 is fixed.
+
+// Wait until an action of `type` is dispatched. If it's part of an
+// async operation, wait until the `status` field is "done" or "error"
+function _afterDispatchDone(store, type) {
+ return new Promise(resolve => {
+ store.dispatch({
+ // Normally we would use `services.WAIT_UNTIL`, but use the
+ // internal name here so tests aren't forced to always pass it
+ // in
+ type: "@@service/waitUntil",
+ predicate: action => {
+ if (action.type === type) {
+ return action.status ?
+ (action.status === "end" || action.status === "error") :
+ true;
+ }
+ return false;
+ },
+ run: (dispatch, getState, action) => {
+ resolve(action);
+ }
+ });
+ });
+}
+
+function waitForDispatch(panel, type, eventRepeat = 1) {
+ const store = panel.panelWin.view.mainFrame.store;
+ const actionType = constants[type];
+ let count = 0;
+
+ return Task.spawn(function* () {
+ info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
+ while (count < eventRepeat) {
+ yield _afterDispatchDone(store, actionType);
+ count++;
+ info(type + " dispatched " + count + " time(s)");
+ }
+ });
+}
diff --git a/devtools/client/dom/test/page_array.html b/devtools/client/dom/test/page_array.html
new file mode 100644
index 000000000..703b93a85
--- /dev/null
+++ b/devtools/client/dom/test/page_array.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>DOM Panel Array Expansion Test Page</title>
+ </head>
+ <body>
+ <h2>DOM Panel Array Expansion Test Page</h2>
+ <script type="text/javascript">
+ "use strict";
+ window._a = [
+ "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"
+ ];
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/dom/test/page_basic.html b/devtools/client/dom/test/page_basic.html
new file mode 100644
index 000000000..170b3112a
--- /dev/null
+++ b/devtools/client/dom/test/page_basic.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>DOM test page</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ window._a = {_data: "test"};
+ </script>
+ </body>
+</html>