summaryrefslogtreecommitdiffstats
path: root/devtools/client/dom
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/dom
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/dom')
-rw-r--r--devtools/client/dom/.eslintrc.js17
-rw-r--r--devtools/client/dom/content/actions/filter.js19
-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/DomTree.js132
-rw-r--r--devtools/client/dom/content/components/MainFrame.js74
-rw-r--r--devtools/client/dom/content/components/MainToolbar.js76
-rw-r--r--devtools/client/dom/content/components/moz.build6
-rw-r--r--devtools/client/dom/content/constants.js7
-rw-r--r--devtools/client/dom/content/dom-decorator.js46
-rw-r--r--devtools/client/dom/content/dom-view.css109
-rw-r--r--devtools/client/dom/content/dom-view.js65
-rw-r--r--devtools/client/dom/content/grip-provider.js100
-rw-r--r--devtools/client/dom/content/moz.build18
-rw-r--r--devtools/client/dom/content/reducers/filter.js27
-rw-r--r--devtools/client/dom/content/reducers/grips.js123
-rw-r--r--devtools/client/dom/content/reducers/index.js12
-rw-r--r--devtools/client/dom/content/reducers/moz.build10
-rw-r--r--devtools/client/dom/content/utils.js25
-rw-r--r--devtools/client/dom/index.html20
-rw-r--r--devtools/client/dom/main.js26
-rw-r--r--devtools/client/dom/moz.build17
-rw-r--r--devtools/client/dom/panel.js258
-rw-r--r--devtools/client/dom/test/.eslintrc.js6
-rw-r--r--devtools/client/dom/test/browser.ini17
-rw-r--r--devtools/client/dom/test/browser_dom_array.js66
-rw-r--r--devtools/client/dom/test/browser_dom_basic.js22
-rw-r--r--devtools/client/dom/test/browser_dom_fission_target_switching.js40
-rw-r--r--devtools/client/dom/test/browser_dom_nodes_highlight.js73
-rw-r--r--devtools/client/dom/test/browser_dom_nodes_select.js43
-rw-r--r--devtools/client/dom/test/browser_dom_refresh.js23
-rw-r--r--devtools/client/dom/test/head.js243
-rw-r--r--devtools/client/dom/test/page_array.html19
-rw-r--r--devtools/client/dom/test/page_basic.html15
-rw-r--r--devtools/client/dom/test/page_dom_nodes.html18
35 files changed, 1835 insertions, 0 deletions
diff --git a/devtools/client/dom/.eslintrc.js b/devtools/client/dom/.eslintrc.js
new file mode 100644
index 0000000000..8e2ca9a042
--- /dev/null
+++ b/devtools/client/dom/.eslintrc.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+module.exports = {
+ globals: {
+ XMLHttpRequest: true,
+ window: true,
+ define: true,
+ addEventListener: true,
+ document: true,
+ dispatchEvent: true,
+ MessageEvent: true,
+ },
+};
diff --git a/devtools/client/dom/content/actions/filter.js b/devtools/client/dom/content/actions/filter.js
new file mode 100644
index 0000000000..83de5a95ce
--- /dev/null
+++ b/devtools/client/dom/content/actions/filter.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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("devtools/client/dom/content/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 0000000000..a49f01f566
--- /dev/null
+++ b/devtools/client/dom/content/actions/grips.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/. */
+/* globals DomProvider */
+"use strict";
+
+const constants = require("devtools/client/dom/content/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 async ({ dispatch }) => {
+ try {
+ // Use 'DomProvider' object exposed from the chrome scope.
+ const response = await DomProvider.getPrototypeAndProperties(grip);
+ dispatch(receiveProperties(grip, response));
+ } catch (e) {
+ console.error("Error while fetching properties", e);
+ }
+ DomProvider.onPropertiesFetched();
+ };
+}
+
+// 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 0000000000..8c1c56d39a
--- /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/DomTree.js b/devtools/client/dom/content/components/DomTree.js
new file mode 100644
index 0000000000..88faa6f2a8
--- /dev/null
+++ b/devtools/client/dom/content/components/DomTree.js
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 DomProvider */
+
+"use strict";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const TreeView = createFactory(
+ require("devtools/client/shared/components/tree/TreeView")
+);
+// Reps
+const { REPS, MODE } = require("devtools/client/shared/components/reps/index");
+const { Rep } = REPS;
+
+const Grip = REPS.Grip;
+// DOM Panel
+const { GripProvider } = require("devtools/client/dom/content/grip-provider");
+
+const { DomDecorator } = require("devtools/client/dom/content/dom-decorator");
+
+/**
+ * Renders DOM panel tree.
+ */
+class DomTree extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ filter: PropTypes.string,
+ grips: PropTypes.object,
+ object: PropTypes.any,
+ openLink: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onFilter = this.onFilter.bind(this);
+ }
+
+ /**
+ * Filter DOM properties. Return true if the object
+ * should be visible in the tree.
+ */
+ onFilter(object) {
+ if (!this.props.filter) {
+ return true;
+ }
+
+ return object.name && object.name.indexOf(this.props.filter) > -1;
+ }
+
+ /**
+ * Render DOM panel content
+ */
+ render() {
+ const { dispatch, grips, object, openLink } = this.props;
+
+ const columns = [
+ {
+ id: "value",
+ },
+ ];
+
+ let onDOMNodeMouseOver;
+ let onDOMNodeMouseOut;
+ let onInspectIconClick;
+ const toolbox = DomProvider.getToolbox();
+ if (toolbox) {
+ const highlighter = toolbox.getHighlighter();
+ onDOMNodeMouseOver = async (grip, options = {}) => {
+ return highlighter.highlight(grip, options);
+ };
+ onDOMNodeMouseOut = async () => {
+ return highlighter.unhighlight();
+ };
+ onInspectIconClick = async grip => {
+ return toolbox.viewElementInInspector(grip, "inspect_dom");
+ };
+ }
+
+ // 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.
+ const renderValue = props => {
+ const repProps = Object.assign({}, props, {
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ onInspectIconClick,
+ defaultRep: Grip,
+ cropLimit: 50,
+ });
+
+ // Object can be an objectFront, while Rep always expect grips.
+ if (props?.object?.getGrip) {
+ repProps.object = props.object.getGrip();
+ }
+
+ return Rep(repProps);
+ };
+
+ return TreeView({
+ columns,
+ decorator: new DomDecorator(),
+ mode: MODE.SHORT,
+ object,
+ onFilter: this.onFilter,
+ openLink,
+ provider: new GripProvider(grips, dispatch),
+ renderValue,
+ });
+ }
+}
+
+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/MainFrame.js b/devtools/client/dom/content/components/MainFrame.js
new file mode 100644
index 0000000000..16aee4856e
--- /dev/null
+++ b/devtools/client/dom/content/components/MainFrame.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals DomProvider */
+
+"use strict";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+// DOM Panel
+const DomTree = createFactory(
+ require("devtools/client/dom/content/components/DomTree")
+);
+
+const MainToolbar = createFactory(
+ require("devtools/client/dom/content/components/MainToolbar")
+);
+// Shortcuts
+const { div } = dom;
+
+/**
+ * Renders basic layout of the DOM panel. The DOM panel content consists
+ * from two main parts: toolbar and tree.
+ */
+class MainFrame extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ filter: PropTypes.string,
+ object: PropTypes.any,
+ };
+ }
+
+ /**
+ * Render DOM panel content
+ */
+ render() {
+ const { filter, object } = this.props;
+
+ return div(
+ { className: "mainFrame" },
+ MainToolbar({
+ dispatch: this.props.dispatch,
+ object: this.props.object,
+ }),
+ div(
+ { className: "treeTableBox devtools-monospace" },
+ DomTree({
+ filter,
+ object,
+ openLink: url => DomProvider.openLink(url),
+ })
+ )
+ );
+ }
+}
+
+// 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/MainToolbar.js b/devtools/client/dom/content/components/MainToolbar.js
new file mode 100644
index 0000000000..342ba97e47
--- /dev/null
+++ b/devtools/client/dom/content/components/MainToolbar.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const SearchBox = createFactory(
+ require("devtools/client/shared/components/SearchBox")
+);
+
+const { l10n } = require("devtools/client/dom/content/utils");
+
+// Actions
+const {
+ fetchProperties,
+} = require("devtools/client/dom/content/actions/grips");
+const {
+ setVisibilityFilter,
+} = require("devtools/client/dom/content/actions/filter");
+
+/**
+ * This template is responsible for rendering a toolbar
+ * within the 'Headers' panel.
+ */
+class MainToolbar extends Component {
+ static get propTypes() {
+ return {
+ object: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onRefresh = this.onRefresh.bind(this);
+ this.onSearch = this.onSearch.bind(this);
+ }
+
+ onRefresh() {
+ this.props.dispatch(fetchProperties(this.props.object));
+ }
+
+ onSearch(value) {
+ this.props.dispatch(setVisibilityFilter(value));
+ }
+
+ render() {
+ return dom.div(
+ { className: "devtools-toolbar devtools-input-toolbar" },
+ SearchBox({
+ key: "filter",
+ delay: 250,
+ onChange: this.onSearch,
+ placeholder: l10n.getStr("dom.filterDOMPanel"),
+ type: "filter",
+ }),
+ dom.span({ className: "devtools-separator" }),
+ dom.button({
+ key: "refresh",
+ className: "refresh devtools-button",
+ id: "dom-refresh-button",
+ title: l10n.getStr("dom.refresh"),
+ onClick: this.onRefresh,
+ })
+ );
+ }
+}
+
+// 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 0000000000..5b669bac24
--- /dev/null
+++ b/devtools/client/dom/content/components/moz.build
@@ -0,0 +1,6 @@
+# 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("DomTree.js", "MainFrame.js", "MainToolbar.js")
diff --git a/devtools/client/dom/content/constants.js b/devtools/client/dom/content/constants.js
new file mode 100644
index 0000000000..c7bb8b325c
--- /dev/null
+++ b/devtools/client/dom/content/constants.js
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 0000000000..39c18330da
--- /dev/null
+++ b/devtools/client/dom/content/dom-decorator.js
@@ -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/. */
+"use strict";
+
+const { Property } = require("devtools/client/dom/content/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) {
+ const value = object.value;
+ const 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 0000000000..d2fd77e525
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.css
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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;
+ background-color: var(--theme-body-background);
+}
+
+.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/lock.svg") no-repeat;
+ background-position: 1px 4px;
+ background-size: 12px 12px;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+}
+
+.treeTable tr:not(.writable) td.treeValueCell:dir(rtl) {
+ background-position-x: right 1px;
+}
+
+.treeTable tr:not(.writable).selected td.treeValueCell {
+ fill: var(--theme-selection-color);
+}
+
+/* Non-enumerable properties are grayed out */
+.treeTable tr:not(.enumerable) td.treeValueCell {
+ opacity: 0.7;
+}
+
+.theme-light .treeTable > tbody > tr > td {
+ border-bottom: 1px solid var(--grey-20);
+}
+
+/* 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;
+}
+
+/******************************************************************************/
+/* Refresh button */
+#dom-refresh-button::before {
+ background-image: url("chrome://devtools/skin/images/reload.svg");
+}
diff --git a/devtools/client/dom/content/dom-view.js b/devtools/client/dom/content/dom-view.js
new file mode 100644
index 0000000000..69aa489279
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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");
+
+// DOM Panel
+const MainFrame = React.createFactory(
+ require("devtools/client/dom/content/components/MainFrame")
+);
+
+// Store
+const createStore = require("devtools/client/shared/redux/create-store");
+
+const { reducers } = require("devtools/client/dom/content/reducers/index");
+const store = createStore(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) {
+ const content = document.querySelector("#content");
+ const mainFrame = MainFrame({
+ object: rootGrip,
+ });
+
+ // Render top level component
+ const provider = React.createElement(
+ Provider,
+ {
+ store: this.store,
+ },
+ mainFrame
+ );
+
+ this.mainFrame = ReactDOM.render(provider, content);
+ },
+
+ onMessage: function(event) {
+ const data = event.data;
+ const 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 through: |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 0000000000..1988369a77
--- /dev/null
+++ b/devtools/client/dom/content/grip-provider.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+ fetchProperties,
+} = require("devtools/client/dom/content/actions/grips");
+const { Property } = require("devtools/client/dom/content/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.actorID) {
+ return [];
+ }
+
+ const props = this.grips.get(grip.actorID);
+ 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) {
+ const value = this.getValue(object);
+ if (!value) {
+ return false;
+ }
+ const grip = value?.getGrip ? value.getGrip() : value;
+
+ let hasChildren = grip.ownPropertyLength > 0;
+
+ if (grip.preview) {
+ hasChildren = hasChildren || grip.preview.ownPropertiesLength > 0;
+ }
+
+ if (grip.preview) {
+ const preview = grip.preview;
+ const k = preview.kind;
+ const objectsWithProps = ["DOMNode", "ObjectWithURL"];
+ hasChildren = hasChildren || objectsWithProps.includes(k);
+ hasChildren = hasChildren || (k == "ArrayLike" && preview.length > 0);
+ }
+
+ return grip.type == "object" && hasChildren;
+ }
+
+ return null;
+ },
+
+ getValue: function(object) {
+ if (object instanceof Property) {
+ const 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) {
+ const grip = object?.getGrip ? object.getGrip() : object;
+ return grip.class ? grip.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 0000000000..04e0ac4efc
--- /dev/null
+++ b/devtools/client/dom/content/moz.build
@@ -0,0 +1,18 @@
+# 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.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 0000000000..428dca2e4c
--- /dev/null
+++ b/devtools/client/dom/content/reducers/filter.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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("devtools/client/dom/content/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 0000000000..acfb1146f2
--- /dev/null
+++ b/devtools/client/dom/content/reducers/grips.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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("devtools/client/dom/content/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) {
+ const response = action.response;
+ const from = response.from;
+ const className = action.grip?.getGrip
+ ? action.grip.getGrip().class
+ : action.grip.class;
+
+ // Properly deal with getters.
+ mergeProperties(response);
+
+ // Compute list of requested children.
+ const previewProps = response.preview ? response.preview.ownProperties : null;
+ const ownProps = response.ownProperties || previewProps || [];
+
+ const 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.
+ const newCache = new Map(cache);
+ newCache.set(from, props);
+
+ return newCache;
+}
+
+// Helpers
+
+function mergeProperties(response) {
+ const { ownProperties } = response;
+
+ // 'safeGetterValues' is new and isn't necessary defined on old grips.
+ const safeGetterValues = response.safeGetterValues || {};
+
+ // Merge the safe getter values into one object such that we can use it
+ // in variablesView.
+ for (const name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ const { 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 0000000000..a6d1fa47b8
--- /dev/null
+++ b/devtools/client/dom/content/reducers/index.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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("devtools/client/dom/content/reducers/grips");
+const { filter } = require("devtools/client/dom/content/reducers/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 0000000000..8d98444e34
--- /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 0000000000..c0632d0e97
--- /dev/null
+++ b/devtools/client/dom/content/utils.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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) {
+ const 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/index.html b/devtools/client/dom/index.html
new file mode 100644
index 0000000000..0e4fd84b76
--- /dev/null
+++ b/devtools/client/dom/index.html
@@ -0,0 +1,20 @@
+<?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="chrome://devtools/content/dom/content/dom-view.css" rel="stylesheet" />
+ <link href="chrome://devtools-jsonview-styles/content/toolbar.css" rel="stylesheet" />
+ <link href="chrome://devtools/content/shared/components/tree/TreeView.css" rel="stylesheet" />
+
+ <script src="chrome://devtools/content/shared/theme-switching.js"></script>
+</head>
+<body class="theme-body" role="application">
+ <div id="content"></div>
+ <script 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 0000000000..efcb1a2b8e
--- /dev/null
+++ b/devtools/client/dom/main.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { BrowserLoader } = ChromeUtils.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("devtools/client/dom/content/dom-view.js");
diff --git a/devtools/client/dom/moz.build b/devtools/client/dom/moz.build
new file mode 100644
index 0000000000..6e4380260a
--- /dev/null
+++ b/devtools/client/dom/moz.build
@@ -0,0 +1,17 @@
+# 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(
+ "panel.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "DOM")
diff --git a/devtools/client/dom/panel.js b/devtools/client/dom/panel.js
new file mode 100644
index 0000000000..a6110748a5
--- /dev/null
+++ b/devtools/client/dom/panel.js
@@ -0,0 +1,258 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 EventEmitter = require("devtools/shared/event-emitter");
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "devtools/client/shared/link",
+ true
+);
+
+/**
+ * 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.onTargetAvailable = this.onTargetAvailable.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.
+ */
+ async open() {
+ // Wait for the retrieval of root object properties before resolving open
+ const onGetProperties = new Promise(resolve => {
+ this._resolveOpen = resolve;
+ });
+
+ this.initialize();
+
+ await onGetProperties;
+
+ this.isReady = true;
+ this.emit("ready");
+
+ return this;
+ },
+
+ // Initialization
+
+ initialize: function() {
+ this.panelWin.addEventListener(
+ "devtools/content/message",
+ this.onContentMessage,
+ true
+ );
+
+ this._toolbox.on("select", this.onPanelVisibilityChange);
+
+ this._toolbox.targetList.watchTargets(
+ [this._toolbox.targetList.TYPES.FRAME],
+ this.onTargetAvailable
+ );
+
+ // Export provider object with useful API for DOM panel.
+ const provider = {
+ getToolbox: this.getToolbox.bind(this),
+ getPrototypeAndProperties: this.getPrototypeAndProperties.bind(this),
+ openLink: this.openLink.bind(this),
+ // Resolve DomPanel.open once the object properties are fetched
+ onPropertiesFetched: () => {
+ if (this._resolveOpen) {
+ this._resolveOpen();
+ this._resolveOpen = null;
+ }
+ },
+ };
+
+ exportIntoContentScope(this.panelWin, provider, "DomProvider");
+ },
+
+ destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ this.currentTarget.off("navigate", this.onTabNavigated);
+ this._toolbox.off("select", this.onPanelVisibilityChange);
+
+ this.emit("destroyed");
+ },
+
+ // 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 navigation occurs.
+ * The panel is refreshed immediately if it's currently selected or lazily when the user
+ * actually selects it.
+ */
+ onTabNavigated: function() {
+ this.shouldRefresh = true;
+ this.refresh();
+ },
+
+ onTargetAvailable: function({ targetFront }) {
+ // Only care about top-level targets.
+ if (!targetFront.isTopLevel) {
+ return;
+ }
+
+ this.shouldRefresh = true;
+ this.refresh();
+
+ // Whenever a new target is available, listen to navigate events on it so we can
+ // refresh the panel when we navigate within the same process.
+ this.currentTarget.on("navigate", this.onTabNavigated);
+ },
+
+ /**
+ * 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: async function(objectFront) {
+ if (!objectFront.actorID) {
+ console.error("No actor!", objectFront);
+ throw new Error("Failed to get object front.");
+ }
+
+ // Bail out if target doesn't exist (toolbox maybe closed already).
+ if (!this.currentTarget) {
+ return null;
+ }
+
+ // Check for a previously stored request for grip.
+ let request = this.pendingRequests.get(objectFront.actorID);
+
+ // If no request is in progress create a new one.
+ if (!request) {
+ request = objectFront.getPrototypeAndProperties();
+ this.pendingRequests.set(objectFront.actorID, request);
+ }
+
+ const response = await request;
+ this.pendingRequests.delete(objectFront.actorID);
+
+ // Fire an event about not having any pending requests.
+ if (!this.pendingRequests.size) {
+ this.emit("no-pending-requests");
+ }
+
+ return response;
+ },
+
+ openLink: function(url) {
+ openContentLink(url);
+ },
+
+ getRootGrip: async function() {
+ // Attach Console. It might involve RDP communication, so wait
+ // asynchronously for the result
+ const consoleFront = await this.currentTarget.getFront("console");
+ const { result } = await consoleFront.evaluateJSAsync("window");
+ return result;
+ },
+
+ postContentMessage: function(type, args) {
+ const data = {
+ type: type,
+ args: args,
+ };
+
+ const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
+ bubbles: true,
+ cancelable: true,
+ data: data,
+ });
+
+ this.panelWin.dispatchEvent(event);
+ },
+
+ onContentMessage: function(event) {
+ const data = event.data;
+ const method = data.type;
+ if (typeof this[method] == "function") {
+ this[method](data.args);
+ }
+ },
+
+ getToolbox: function() {
+ return this._toolbox;
+ },
+
+ get currentTarget() {
+ return this._toolbox.target;
+ },
+};
+
+// Helpers
+
+function exportIntoContentScope(win, obj, defineAs) {
+ const clone = Cu.createObjectIn(win, {
+ defineAs: defineAs,
+ });
+
+ const props = Object.getOwnPropertyNames(obj);
+ for (let i = 0; i < props.length; i++) {
+ const propName = props[i];
+ const 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/test/.eslintrc.js b/devtools/client/dom/test/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /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 0000000000..067664afb3
--- /dev/null
+++ b/devtools/client/dom/test/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ page_array.html
+ page_basic.html
+ page_dom_nodes.html
+ !/devtools/client/shared/test/shared-head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_dom_array.js]
+[browser_dom_basic.js]
+[browser_dom_fission_target_switching.js]
+[browser_dom_nodes_highlight.js]
+[browser_dom_nodes_select.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 0000000000..44fba5e11e
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_array.js
@@ -0,0 +1,66 @@
+/* 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(async function() {
+ info("Test DOM Panel Array Expansion started");
+
+ const { panel } = await addTestTab(TEST_PAGE_URL);
+
+ // Expand specified row and wait till children are displayed.
+ await expandRow(panel, "_a");
+
+ // Verify that children is displayed now.
+ const childRows = getAllRowsForLabel(panel, "_a");
+
+ const item = childRows.pop();
+ is(item.name, "length", "length property is correct");
+ is(item.value, 26, "length property value is 26");
+
+ let i = 0;
+ for (const name in childRows) {
+ const row = childRows[name];
+
+ is(
+ parseInt(name, 10),
+ 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 0000000000..532a6da3fd
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_basic.js
@@ -0,0 +1,22 @@
+/* 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(async function() {
+ info("Test DOM panel basic started");
+
+ const { panel } = await addTestTab(TEST_PAGE_URL);
+
+ // Expand specified row and wait till children are displayed.
+ await expandRow(panel, "_a");
+
+ // Verify that child is displayed now.
+ const childRow = getRowByLabel(panel, "_data");
+ ok(childRow, "Child row must exist");
+});
diff --git a/devtools/client/dom/test/browser_dom_fission_target_switching.js b/devtools/client/dom/test/browser_dom_fission_target_switching.js
new file mode 100644
index 0000000000..af9ca13f67
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_fission_target_switching.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test top-level target switching in the DOM panel.
+
+const PARENT_PROCESS_URI = "about:robots";
+const CONTENT_PROCESS_URI = URL_ROOT + "page_basic.html";
+
+add_task(async function() {
+ // We use about:robots as the starting page because it will run in the parent process.
+ // Navigating from that page to a regular content page will always trigger a target
+ // switch, with or without fission.
+
+ info("Open a page that runs in the parent process");
+ const { panel } = await addTestTab(PARENT_PROCESS_URI);
+
+ const _aProperty = getRowByLabel(panel, "_a");
+ let buttonProperty = getRowByLabel(panel, "button");
+
+ ok(!_aProperty, "There is no _a property on the about:robots page");
+ ok(buttonProperty, "There is, however, a button property on this page");
+
+ info("Navigate to a page that runs in the content process");
+ // Wait for the DOM panel to refresh.
+ const onPropertiesFetched = waitForDispatch(panel, "FETCH_PROPERTIES");
+ // Also wait for the toolbox to switch to the new target, to avoid hanging requests when
+ // the test ends.
+ await navigateTo(CONTENT_PROCESS_URI);
+ await onPropertiesFetched;
+
+ await waitFor(() => getRowByLabel(panel, "_a"));
+ ok(true, "This time, the _a property exists on this content process page");
+
+ buttonProperty = getRowByLabel(panel, "button");
+ ok(
+ !buttonProperty,
+ "There is, however, no more button property on this page"
+ );
+});
diff --git a/devtools/client/dom/test/browser_dom_nodes_highlight.js b/devtools/client/dom/test/browser_dom_nodes_highlight.js
new file mode 100644
index 0000000000..7f880a3a9b
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_nodes_highlight.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_dom_nodes.html";
+
+/**
+ * Checks that hovering nodes highlights them in the content page
+ */
+add_task(async function() {
+ info("Test DOM panel node highlight started");
+
+ const { panel } = await addTestTab(TEST_PAGE_URL);
+ const toolbox = gDevTools.getToolbox(panel.currentTarget);
+ const highlighter = toolbox.getHighlighter();
+
+ const tests = [
+ {
+ expected: "h1",
+ getNode: async () => {
+ return getRowByIndex(panel, 2).querySelector(".objectBox-node");
+ },
+ },
+ {
+ expected: "h2",
+ getNode: async () => {
+ info("Expand specified row and wait till children are displayed");
+ await expandRow(panel, "_b");
+ return getRowByIndex(panel, 3).querySelector(".objectBox-node");
+ },
+ },
+ ];
+
+ for (const test of tests) {
+ info(`Get the NodeFront for ${test.expected}`);
+ const node = await test.getNode();
+
+ info("Highlight the node by moving the cursor on it");
+ const onHighlighterShown = highlighter.waitForHighlighterShown();
+ EventUtils.synthesizeMouseAtCenter(
+ node,
+ {
+ type: "mouseover",
+ },
+ node.ownerDocument.defaultView
+ );
+ const { nodeFront } = await onHighlighterShown;
+ is(
+ nodeFront.displayName,
+ test.expected,
+ "The correct node was highlighted"
+ );
+
+ info("Unhighlight the node by moving the cursor away from it");
+ const onHighlighterHidden = highlighter.waitForHighlighterHidden();
+ const btn = toolbox.doc.querySelector("#toolbox-meatball-menu-button");
+ EventUtils.synthesizeMouseAtCenter(
+ btn,
+ {
+ type: "mouseover",
+ },
+ btn.ownerDocument.defaultView
+ );
+
+ const { nodeFront: unhighlightedNode } = await onHighlighterHidden;
+ is(
+ unhighlightedNode.displayName,
+ test.expected,
+ "The node was unhighlighted"
+ );
+ }
+});
diff --git a/devtools/client/dom/test/browser_dom_nodes_select.js b/devtools/client/dom/test/browser_dom_nodes_select.js
new file mode 100644
index 0000000000..8b7724294d
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_nodes_select.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_dom_nodes.html";
+
+/**
+ * Checks whether hovering nodes highlight them in the content page
+ */
+add_task(async function() {
+ info("Test DOM panel node highlight started");
+
+ const { panel } = await addTestTab(TEST_PAGE_URL);
+ const toolbox = gDevTools.getToolbox(panel.currentTarget);
+ const node = getRowByIndex(panel, 2);
+
+ // Loading the inspector panel at first, to make it possible to listen for
+ // new node selections
+
+ await toolbox.loadTool("inspector");
+ const inspector = toolbox.getPanel("inspector");
+
+ const openInInspectorIcon = node.querySelector(".open-inspector");
+ ok(node !== null, "Node was logged as expected");
+
+ info(
+ "Clicking on the inspector icon and waiting for the " +
+ "inspector to be selected"
+ );
+ const onInspectorSelected = toolbox.once("inspector-selected");
+ const onInspectorUpdated = inspector.once("inspector-updated");
+ const onNewNode = toolbox.selection.once("new-node-front");
+
+ openInInspectorIcon.click();
+
+ await onInspectorSelected;
+ await onInspectorUpdated;
+ const nodeFront = await onNewNode;
+
+ ok(true, "Inspector selected and new node got selected");
+ is(nodeFront.displayName, "h1", "The expected node was selected");
+});
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 0000000000..905f15e873
--- /dev/null
+++ b/devtools/client/dom/test/browser_dom_refresh.js
@@ -0,0 +1,23 @@
+/* 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(async function() {
+ info("Test DOM panel basic started");
+
+ const { panel } = await addTestTab(TEST_PAGE_URL);
+
+ // Create a new variable in the page scope and refresh the panel.
+ await evaluateJSAsync(panel, "var _b = 10");
+ await refreshPanel(panel);
+
+ // Verify that the variable is displayed now.
+ const 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 0000000000..d812e96187
--- /dev/null
+++ b/devtools/client/dom/test/head.js
@@ -0,0 +1,243 @@
+/* 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 ../../shared/test/shared-head.js */
+
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/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
+ */
+async function addTestTab(url) {
+ info("Adding a new test tab with URL: '" + url + "'");
+
+ const tab = await addTab(url);
+
+ // Select the DOM panel and wait till it's initialized.
+ const panel = await initDOMPanel(tab);
+
+ // FETCH_PROPERTIES should be fired during the call to initDOMPanel
+ // But note that this behavior changed during a change in webconsole
+ // initialization. So this might be racy.
+ const doc = panel.panelWin.document;
+ const nodes = [...doc.querySelectorAll(".treeLabel")];
+ ok(nodes.length > 0, "The DOM panel is already populated");
+
+ return {
+ tab,
+ browser: tab.linkedBrowser,
+ panel,
+ };
+}
+
+/**
+ * Open the DOM panel for the given tab.
+ *
+ * @param {Element} 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.
+ */
+async function initDOMPanel(tab) {
+ const target = await TargetFactory.forTab(tab || gBrowser.selectedTab);
+ const toolbox = await gDevTools.showToolbox(target, "dom");
+ const panel = toolbox.getCurrentPanel();
+ return 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) {
+ const doc = panel.panelWin.document;
+ const labels = [...doc.querySelectorAll(".treeLabel")];
+ const label = labels.find(node => node.textContent == text);
+ return label ? label.closest(".treeRow") : null;
+}
+
+/**
+ * Returns tree row with specified index.
+ */
+function getRowByIndex(panel, id) {
+ const doc = panel.panelWin.document;
+ const labels = [...doc.querySelectorAll(".treeLabel")];
+ const label = labels.find((node, i) => i == id);
+ 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;
+ const result = [];
+ const doc = panel.panelWin.document;
+ const 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) {
+ const 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) {
+ const row = getRowByLabel(panel, labelText);
+ return synthesizeMouseClickSoon(panel, row).then(() => {
+ // Wait till children (properties) are fetched
+ // from the backend.
+ return waitForDispatch(panel, "FETCH_PROPERTIES");
+ });
+}
+
+async function evaluateJSAsync(panel, expression) {
+ const consoleFront = await panel.currentTarget.getFront("console");
+ return consoleFront.evaluateJSAsync(expression);
+}
+
+function refreshPanel(panel) {
+ const doc = panel.panelWin.document;
+ const button = doc.querySelector("#dom-refresh-button");
+ 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 (async function() {
+ info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
+ while (count < eventRepeat) {
+ await _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 0000000000..848d6c4ce5
--- /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 0000000000..170b3112a6
--- /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>
diff --git a/devtools/client/dom/test/page_dom_nodes.html b/devtools/client/dom/test/page_dom_nodes.html
new file mode 100644
index 0000000000..33d0655f9d
--- /dev/null
+++ b/devtools/client/dom/test/page_dom_nodes.html
@@ -0,0 +1,18 @@
+<!-- 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 hovering nodes page</title>
+ </head>
+ <body>
+ <h1 id="a">Node highlight test</h1>
+ <h2 id="b">Node highlight test inside object</h2>
+ <script>
+ "use strict";
+ window._a = document.getElementById("a");
+ window._b = {_data: document.getElementById("b")};
+ </script>
+ </body>
+</html>