summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/extensions
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/extensions/actions/index.js24
-rw-r--r--devtools/client/inspector/extensions/actions/moz.build10
-rw-r--r--devtools/client/inspector/extensions/actions/sidebar.js58
-rw-r--r--devtools/client/inspector/extensions/components/ExpressionResultView.js110
-rw-r--r--devtools/client/inspector/extensions/components/ExtensionPage.js56
-rw-r--r--devtools/client/inspector/extensions/components/ExtensionSidebar.js106
-rw-r--r--devtools/client/inspector/extensions/components/ObjectTreeView.js67
-rw-r--r--devtools/client/inspector/extensions/components/moz.build12
-rw-r--r--devtools/client/inspector/extensions/extension-sidebar.js189
-rw-r--r--devtools/client/inspector/extensions/moz.build18
-rw-r--r--devtools/client/inspector/extensions/reducers/moz.build9
-rw-r--r--devtools/client/inspector/extensions/reducers/sidebar.js67
-rw-r--r--devtools/client/inspector/extensions/test/browser.toml15
-rw-r--r--devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js451
-rw-r--r--devtools/client/inspector/extensions/test/head.js21
-rw-r--r--devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js224
-rw-r--r--devtools/client/inspector/extensions/types.js15
17 files changed, 1452 insertions, 0 deletions
diff --git a/devtools/client/inspector/extensions/actions/index.js b/devtools/client/inspector/extensions/actions/index.js
new file mode 100644
index 0000000000..384dfb0da7
--- /dev/null
+++ b/devtools/client/inspector/extensions/actions/index.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { createEnum } = require("resource://devtools/client/shared/enum.js");
+
+createEnum(
+ [
+ // Update the extension sidebar with an object TreeView.
+ "EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE",
+
+ // Update the extension sidebar with an expression result.
+ "EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE",
+
+ // Switch the extension sidebar into an extension page container.
+ "EXTENSION_SIDEBAR_PAGE_UPDATE",
+
+ // Remove an extension sidebar from the inspector store.
+ "EXTENSION_SIDEBAR_REMOVE",
+ ],
+ module.exports
+);
diff --git a/devtools/client/inspector/extensions/actions/moz.build b/devtools/client/inspector/extensions/actions/moz.build
new file mode 100644
index 0000000000..d101bc2fea
--- /dev/null
+++ b/devtools/client/inspector/extensions/actions/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "index.js",
+ "sidebar.js",
+)
diff --git a/devtools/client/inspector/extensions/actions/sidebar.js b/devtools/client/inspector/extensions/actions/sidebar.js
new file mode 100644
index 0000000000..34c7276c27
--- /dev/null
+++ b/devtools/client/inspector/extensions/actions/sidebar.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+ EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE,
+ EXTENSION_SIDEBAR_PAGE_UPDATE,
+ EXTENSION_SIDEBAR_REMOVE,
+} = require("resource://devtools/client/inspector/extensions/actions/index.js");
+
+module.exports = {
+ /**
+ * Update the sidebar with an object treeview.
+ */
+ updateObjectTreeView(sidebarId, object) {
+ return {
+ type: EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+ sidebarId,
+ object,
+ };
+ },
+
+ /**
+ * Update the sidebar with an expression result.
+ */
+ updateExpressionResultView(sidebarId, expressionResult, rootTitle) {
+ return {
+ type: EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE,
+ sidebarId,
+ expressionResult,
+ rootTitle,
+ };
+ },
+
+ /**
+ * Switch the sidebar into the extension page mode.
+ */
+ updateExtensionPage(sidebarId, iframeURL) {
+ return {
+ type: EXTENSION_SIDEBAR_PAGE_UPDATE,
+ sidebarId,
+ iframeURL,
+ };
+ },
+
+ /**
+ * Remove the extension sidebar from the inspector store.
+ */
+ removeExtensionSidebar(sidebarId) {
+ return {
+ type: EXTENSION_SIDEBAR_REMOVE,
+ sidebarId,
+ };
+ },
+};
diff --git a/devtools/client/inspector/extensions/components/ExpressionResultView.js b/devtools/client/inspector/extensions/components/ExpressionResultView.js
new file mode 100644
index 0000000000..887c5610c4
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ExpressionResultView.js
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Accordion = createFactory(
+ require("resource://devtools/client/shared/components/Accordion.js")
+);
+
+const Types = require("resource://devtools/client/inspector/extensions/types.js");
+
+const {
+ REPS: { Grip },
+ MODE,
+ objectInspector: { ObjectInspector: ObjectInspectorClass },
+} = require("resource://devtools/client/shared/components/reps/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "LongStringFront",
+ "resource://devtools/client/fronts/string.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "ObjectFront",
+ "resource://devtools/client/fronts/object.js",
+ true
+);
+
+const ObjectInspector = createFactory(ObjectInspectorClass);
+
+class ObjectValueGripView extends PureComponent {
+ static get propTypes() {
+ return {
+ rootTitle: PropTypes.string,
+ expressionResult: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.object,
+ ]).isRequired,
+ // Helpers injected as props by extension-sidebar.js.
+ serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired,
+ };
+ }
+
+ render() {
+ const { expressionResult, serviceContainer, rootTitle } = this.props;
+
+ const isFront =
+ expressionResult instanceof ObjectFront ||
+ expressionResult instanceof LongStringFront;
+ const grip = isFront ? expressionResult.getGrip() : expressionResult;
+
+ const objectInspectorProps = {
+ autoExpandDepth: 1,
+ mode: MODE.SHORT,
+ // TODO: we disable focus since it's not currently working well in ObjectInspector.
+ // Let's remove the property below when problem are fixed in OI.
+ disabledFocus: true,
+ roots: [
+ {
+ path: expressionResult?.actorID || JSON.stringify(expressionResult),
+ contents: { value: grip, front: isFront ? expressionResult : null },
+ },
+ ],
+ // TODO: evaluate if there should also be a serviceContainer.openLink.
+ };
+
+ if (expressionResult?.actorID) {
+ Object.assign(objectInspectorProps, {
+ onDOMNodeMouseOver: serviceContainer.highlightDomElement,
+ onDOMNodeMouseOut: serviceContainer.unHighlightDomElement,
+ onInspectIconClick(object, e) {
+ // Stop the event propagation so we don't trigger ObjectInspector
+ // expand/collapse.
+ e.stopPropagation();
+ serviceContainer.openNodeInInspector(object);
+ },
+ defaultRep: Grip,
+ });
+ }
+
+ if (rootTitle) {
+ return Accordion({
+ items: [
+ {
+ component: ObjectInspector,
+ componentProps: objectInspectorProps,
+ header: rootTitle,
+ id: rootTitle.replace(/\s/g, "-"),
+ opened: true,
+ },
+ ],
+ });
+ }
+
+ return ObjectInspector(objectInspectorProps);
+ }
+}
+
+module.exports = ObjectValueGripView;
diff --git a/devtools/client/inspector/extensions/components/ExtensionPage.js b/devtools/client/inspector/extensions/components/ExtensionPage.js
new file mode 100644
index 0000000000..3f92b8f41f
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ExtensionPage.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createRef,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+/**
+ * The ExtensionPage React Component is used in the ExtensionSidebar component to provide
+ * a UI viewMode which shows an extension page rendered inside the sidebar panel.
+ */
+class ExtensionPage extends PureComponent {
+ static get propTypes() {
+ return {
+ iframeURL: PropTypes.string.isRequired,
+ onExtensionPageMount: PropTypes.func.isRequired,
+ onExtensionPageUnmount: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.iframeRef = createRef();
+ }
+
+ componentDidMount() {
+ this.props.onExtensionPageMount(this.iframeRef.current);
+ }
+
+ componentWillUnmount() {
+ this.props.onExtensionPageUnmount(this.iframeRef.current);
+ }
+
+ render() {
+ return dom.iframe({
+ className: "inspector-extension-sidebar-page",
+ src: this.props.iframeURL,
+ style: {
+ width: "100%",
+ height: "100%",
+ margin: 0,
+ padding: 0,
+ },
+ ref: this.iframeRef,
+ });
+ }
+}
+
+module.exports = ExtensionPage;
diff --git a/devtools/client/inspector/extensions/components/ExtensionSidebar.js b/devtools/client/inspector/extensions/components/ExtensionSidebar.js
new file mode 100644
index 0000000000..a351b6add9
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ExtensionSidebar.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const ExtensionPage = createFactory(
+ require("resource://devtools/client/inspector/extensions/components/ExtensionPage.js")
+);
+const ObjectTreeView = createFactory(
+ require("resource://devtools/client/inspector/extensions/components/ObjectTreeView.js")
+);
+const ExpressionResultView = createFactory(
+ require("resource://devtools/client/inspector/extensions/components/ExpressionResultView.js")
+);
+const Types = require("resource://devtools/client/inspector/extensions/types.js");
+
+/**
+ * The ExtensionSidebar is a React component with 2 supported viewMode:
+ * - an ObjectTreeView UI, used to show the JS objects
+ * (used by the sidebar.setObject WebExtensions APIs)
+ * - an ExpressionResultView UI, used to show the result for an expression
+ * (used by sidebar.setExpression WebExtensions APIs)
+ * - an ExtensionPage UI used to show an extension page
+ * (used by the sidebar.setPage WebExtensions APIs).
+ *
+ * TODO: implement the ExtensionPage viewMode.
+ */
+class ExtensionSidebar extends PureComponent {
+ static get propTypes() {
+ return {
+ id: PropTypes.string.isRequired,
+ extensionsSidebar: PropTypes.object.isRequired,
+ onExtensionPageMount: PropTypes.func.isRequired,
+ onExtensionPageUnmount: PropTypes.func.isRequired,
+ // Helpers injected as props by extension-sidebar.js.
+ serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired,
+ };
+ }
+
+ render() {
+ const {
+ id,
+ extensionsSidebar,
+ onExtensionPageMount,
+ onExtensionPageUnmount,
+ serviceContainer,
+ } = this.props;
+
+ const {
+ iframeURL,
+ object,
+ expressionResult,
+ rootTitle,
+ viewMode = "empty-sidebar",
+ } = extensionsSidebar[id] || {};
+
+ let sidebarContentEl;
+
+ switch (viewMode) {
+ case "object-treeview":
+ sidebarContentEl = ObjectTreeView({ object });
+ break;
+ case "object-value-grip-view":
+ sidebarContentEl = ExpressionResultView({
+ expressionResult,
+ rootTitle,
+ serviceContainer,
+ });
+ break;
+ case "extension-page":
+ sidebarContentEl = ExtensionPage({
+ iframeURL,
+ onExtensionPageMount,
+ onExtensionPageUnmount,
+ });
+ break;
+ case "empty-sidebar":
+ break;
+ default:
+ throw new Error(`Unknown ExtensionSidebar viewMode: "${viewMode}"`);
+ }
+
+ const className = "devtools-monospace extension-sidebar inspector-tabpanel";
+
+ return dom.div(
+ {
+ id,
+ className,
+ },
+ sidebarContentEl
+ );
+ }
+}
+
+module.exports = connect(state => state)(ExtensionSidebar);
diff --git a/devtools/client/inspector/extensions/components/ObjectTreeView.js b/devtools/client/inspector/extensions/components/ObjectTreeView.js
new file mode 100644
index 0000000000..1788f8d020
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ObjectTreeView.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ REPS,
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/index.js");
+const { Rep } = REPS;
+const TreeViewClass = require("resource://devtools/client/shared/components/tree/TreeView.js");
+const TreeView = createFactory(TreeViewClass);
+
+/**
+ * The ExpressionResultView React Component is used in the ExtensionSidebar component to
+ * provide a UI viewMode which shows a tree view of the passed JavaScript object.
+ */
+class ExpressionResultView extends PureComponent {
+ static get propTypes() {
+ return {
+ object: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const { object } = this.props;
+
+ const columns = [
+ {
+ id: "value",
+ },
+ ];
+
+ // Render the node value (omitted on the root element if it has children).
+ const renderValue = props => {
+ if (props.member.level === 0 && props.member.hasChildren) {
+ return undefined;
+ }
+
+ return Rep(
+ Object.assign({}, props, {
+ cropLimit: 50,
+ })
+ );
+ };
+
+ return TreeView({
+ object,
+ mode: MODE.SHORT,
+ columns,
+ renderValue,
+ expandedNodes: TreeViewClass.getExpandedNodes(object, {
+ maxLevel: 1,
+ maxNodes: 1,
+ }),
+ });
+ }
+}
+
+module.exports = ExpressionResultView;
diff --git a/devtools/client/inspector/extensions/components/moz.build b/devtools/client/inspector/extensions/components/moz.build
new file mode 100644
index 0000000000..62f3d991c1
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "ExpressionResultView.js",
+ "ExtensionPage.js",
+ "ExtensionSidebar.js",
+ "ObjectTreeView.js",
+)
diff --git a/devtools/client/inspector/extensions/extension-sidebar.js b/devtools/client/inspector/extensions/extension-sidebar.js
new file mode 100644
index 0000000000..c359d8b4fa
--- /dev/null
+++ b/devtools/client/inspector/extensions/extension-sidebar.js
@@ -0,0 +1,189 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ createElement,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const {
+ Provider,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const extensionsSidebarReducer = require("resource://devtools/client/inspector/extensions/reducers/sidebar.js");
+const {
+ default: objectInspectorReducer,
+} = require("resource://devtools/client/shared/components/object-inspector/reducer.js");
+
+const ExtensionSidebarComponent = createFactory(
+ require("resource://devtools/client/inspector/extensions/components/ExtensionSidebar.js")
+);
+
+const {
+ updateExtensionPage,
+ updateObjectTreeView,
+ updateExpressionResultView,
+ removeExtensionSidebar,
+} = require("resource://devtools/client/inspector/extensions/actions/sidebar.js");
+
+/**
+ * ExtensionSidebar instances represents Inspector sidebars installed by add-ons
+ * using the devtools.panels.elements.createSidebarPane WebExtensions API.
+ *
+ * The WebExtensions API registers the extensions' sidebars on the toolbox instance
+ * (using the registerInspectorExtensionSidebar method) and, once the Inspector has been
+ * created, the toolbox uses the Inpector createExtensionSidebar method to create the
+ * ExtensionSidebar instances and then it registers them to the Inspector.
+ *
+ * @param {Inspector} inspector
+ * The inspector where the sidebar should be hooked to.
+ * @param {Object} options
+ * @param {String} options.id
+ * The unique id of the sidebar.
+ * @param {String} options.title
+ * The title of the sidebar.
+ */
+class ExtensionSidebar {
+ constructor(inspector, { id, title }) {
+ EventEmitter.decorate(this);
+ this.inspector = inspector;
+ this.store = inspector.store;
+ this.id = id;
+ this.title = title;
+ this.destroyed = false;
+
+ this.store.injectReducer("extensionsSidebar", extensionsSidebarReducer);
+ this.store.injectReducer("objectInspector", objectInspectorReducer);
+ }
+
+ /**
+ * Lazily create a React ExtensionSidebarComponent wrapped into a Redux Provider.
+ */
+ get provider() {
+ if (!this._provider) {
+ this._provider = createElement(
+ Provider,
+ {
+ store: this.store,
+ key: this.id,
+ title: this.title,
+ },
+ ExtensionSidebarComponent({
+ id: this.id,
+ onExtensionPageMount: containerEl => {
+ this.emit("extension-page-mount", containerEl);
+ },
+ onExtensionPageUnmount: containerEl => {
+ this.emit("extension-page-unmount", containerEl);
+ },
+ serviceContainer: {
+ highlightDomElement: async (grip, options = {}) => {
+ const nodeFront =
+ await this.inspector.inspectorFront.getNodeFrontFromNodeGrip(
+ grip
+ );
+ return this.inspector.highlighters.showHighlighterTypeForNode(
+ this.inspector.highlighters.TYPES.BOXMODEL,
+ nodeFront,
+ options
+ );
+ },
+ unHighlightDomElement: async () => {
+ return this.inspector.highlighters.hideHighlighterType(
+ this.inspector.highlighters.TYPES.BOXMODEL
+ );
+ },
+ openNodeInInspector: async grip => {
+ const nodeFront =
+ await this.inspector.inspectorFront.getNodeFrontFromNodeGrip(
+ grip
+ );
+ const onInspectorUpdated =
+ this.inspector.once("inspector-updated");
+ const onNodeFrontSet =
+ this.inspector.toolbox.selection.setNodeFront(nodeFront, {
+ reason: "inspector-extension-sidebar",
+ });
+
+ return Promise.all([onNodeFrontSet, onInspectorUpdated]);
+ },
+ },
+ })
+ );
+ }
+
+ return this._provider;
+ }
+
+ /**
+ * Destroy the ExtensionSidebar instance, dispatch a removeExtensionSidebar Redux action
+ * (which removes the related state from the Inspector store) and clear any reference
+ * to the inspector, the Redux store and the lazily created Redux Provider component.
+ *
+ * This method is called by the inspector when the ExtensionSidebar is being removed
+ * (or when the inspector is being destroyed).
+ */
+ destroy() {
+ if (this.destroyed) {
+ throw new Error(
+ `ExtensionSidebar instances cannot be destroyed more than once`
+ );
+ }
+
+ // Remove the data related to this extension from the inspector store.
+ this.store.dispatch(removeExtensionSidebar(this.id));
+
+ this.inspector = null;
+ this.store = null;
+ this._provider = null;
+
+ this.destroyed = true;
+ }
+
+ /**
+ * Dispatch an objectTreeView action to change the SidebarComponent into an
+ * ObjectTreeView React Component, which shows the passed javascript object
+ * in the sidebar.
+ */
+ setObject(object) {
+ if (this.removed) {
+ throw new Error(
+ "Unable to set an object preview on a removed ExtensionSidebar"
+ );
+ }
+
+ this.store.dispatch(updateObjectTreeView(this.id, object));
+ }
+
+ /**
+ * Dispatch an objectPreview action to change the SidebarComponent into an
+ * ObjectPreview React Component, which shows the passed value grip
+ * in the sidebar.
+ */
+ setExpressionResult(expressionResult, rootTitle) {
+ if (this.removed) {
+ throw new Error(
+ "Unable to set an object preview on a removed ExtensionSidebar"
+ );
+ }
+
+ this.store.dispatch(
+ updateExpressionResultView(this.id, expressionResult, rootTitle)
+ );
+ }
+
+ setExtensionPage(iframeURL) {
+ if (this.removed) {
+ throw new Error(
+ "Unable to set an object preview on a removed ExtensionSidebar"
+ );
+ }
+
+ this.store.dispatch(updateExtensionPage(this.id, iframeURL));
+ }
+}
+
+module.exports = ExtensionSidebar;
diff --git a/devtools/client/inspector/extensions/moz.build b/devtools/client/inspector/extensions/moz.build
new file mode 100644
index 0000000000..39647c8e5a
--- /dev/null
+++ b/devtools/client/inspector/extensions/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "actions",
+ "components",
+ "reducers",
+]
+
+DevToolsModules(
+ "extension-sidebar.js",
+ "types.js",
+)
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
diff --git a/devtools/client/inspector/extensions/reducers/moz.build b/devtools/client/inspector/extensions/reducers/moz.build
new file mode 100644
index 0000000000..0f8a5757c8
--- /dev/null
+++ b/devtools/client/inspector/extensions/reducers/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "sidebar.js",
+)
diff --git a/devtools/client/inspector/extensions/reducers/sidebar.js b/devtools/client/inspector/extensions/reducers/sidebar.js
new file mode 100644
index 0000000000..c7566f3b67
--- /dev/null
+++ b/devtools/client/inspector/extensions/reducers/sidebar.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+ EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE,
+ EXTENSION_SIDEBAR_PAGE_UPDATE,
+ EXTENSION_SIDEBAR_REMOVE,
+} = require("resource://devtools/client/inspector/extensions/actions/index.js");
+
+const INITIAL_SIDEBAR = {};
+
+const reducers = {
+ [EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE](sidebar, { sidebarId, object }) {
+ // Update the sidebar to a "object-treeview" which shows
+ // the passed object.
+ return Object.assign({}, sidebar, {
+ [sidebarId]: {
+ viewMode: "object-treeview",
+ object,
+ },
+ });
+ },
+
+ [EXTENSION_SIDEBAR_EXPRESSION_RESULT_VIEW_UPDATE](
+ sidebar,
+ { sidebarId, expressionResult, rootTitle }
+ ) {
+ // Update the sidebar to a "object-treeview" which shows
+ // the passed object.
+ return Object.assign({}, sidebar, {
+ [sidebarId]: {
+ viewMode: "object-value-grip-view",
+ expressionResult,
+ rootTitle,
+ },
+ });
+ },
+
+ [EXTENSION_SIDEBAR_PAGE_UPDATE](sidebar, { sidebarId, iframeURL }) {
+ // Update the sidebar to a "object-treeview" which shows
+ // the passed object.
+ return Object.assign({}, sidebar, {
+ [sidebarId]: {
+ viewMode: "extension-page",
+ iframeURL,
+ },
+ });
+ },
+
+ [EXTENSION_SIDEBAR_REMOVE](sidebar, { sidebarId }) {
+ // Remove the sidebar from the Redux store.
+ delete sidebar[sidebarId];
+ return Object.assign({}, sidebar);
+ },
+};
+
+module.exports = function (sidebar = INITIAL_SIDEBAR, action) {
+ const reducer = reducers[action.type];
+ if (!reducer) {
+ return sidebar;
+ }
+ return reducer(sidebar, action);
+};
diff --git a/devtools/client/inspector/extensions/test/browser.toml b/devtools/client/inspector/extensions/test/browser.toml
new file mode 100644
index 0000000000..2c6ad4538c
--- /dev/null
+++ b/devtools/client/inspector/extensions/test/browser.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "head.js",
+ "head_devtools_inspector_sidebar.js",
+ "!/devtools/client/inspector/test/head.js",
+ "!/devtools/client/inspector/test/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+]
+
+["browser_inspector_extension_sidebar.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
diff --git a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
new file mode 100644
index 0000000000..740b1fda13
--- /dev/null
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -0,0 +1,451 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SIDEBAR_ID = "an-extension-sidebar";
+const SIDEBAR_TITLE = "Sidebar Title";
+
+let extension;
+let fakeExtCallerInfo;
+
+let toolbox;
+let inspector;
+
+add_task(async function setupExtensionSidebar() {
+ extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // This is just an empty extension used to ensure that the caller extension uuid
+ // actually exists.
+ },
+ });
+
+ await extension.startup();
+
+ fakeExtCallerInfo = {
+ url: WebExtensionPolicy.getByID(extension.id).getURL(
+ "fake-caller-script.js"
+ ),
+ lineNumber: 1,
+ addonId: extension.id,
+ };
+
+ const res = await openInspectorForURL("about:blank");
+ inspector = res.inspector;
+ toolbox = res.toolbox;
+
+ const onceSidebarCreated = toolbox.once(
+ `extension-sidebar-created-${SIDEBAR_ID}`
+ );
+ toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, {
+ title: SIDEBAR_TITLE,
+ });
+
+ const sidebar = await onceSidebarCreated;
+
+ // Test sidebar properties.
+ is(
+ sidebar,
+ inspector.getPanel(SIDEBAR_ID),
+ "Got an extension sidebar instance equal to the one saved in the inspector"
+ );
+ is(
+ sidebar.title,
+ SIDEBAR_TITLE,
+ "Got the expected title in the extension sidebar instance"
+ );
+ is(
+ sidebar.provider.props.title,
+ SIDEBAR_TITLE,
+ "Got the expeted title in the provider props"
+ );
+
+ // Test sidebar Redux state.
+ const inspectorStoreState = inspector.store.getState();
+ ok(
+ "extensionsSidebar" in inspectorStoreState,
+ "Got the extensionsSidebar sub-state in the inspector Redux store"
+ );
+ Assert.deepEqual(
+ inspectorStoreState.extensionsSidebar,
+ {},
+ "The extensionsSidebar should be initially empty"
+ );
+});
+
+add_task(async function testSidebarSetObject() {
+ const object = {
+ propertyName: {
+ nestedProperty: "propertyValue",
+ anotherProperty: "anotherValue",
+ },
+ };
+
+ const sidebar = inspector.getPanel(SIDEBAR_ID);
+ sidebar.setObject(object);
+
+ // Test updated sidebar Redux state.
+ const inspectorStoreState = inspector.store.getState();
+ is(
+ Object.keys(inspectorStoreState.extensionsSidebar).length,
+ 1,
+ "The extensionsSidebar state contains the newly registered extension sidebar state"
+ );
+ Assert.deepEqual(
+ inspectorStoreState.extensionsSidebar,
+ {
+ [SIDEBAR_ID]: {
+ viewMode: "object-treeview",
+ object,
+ },
+ },
+ "Got the expected state for the registered extension sidebar"
+ );
+
+ // Select the extension sidebar.
+ const waitSidebarSelected = toolbox.once(`inspector-sidebar-select`);
+ inspector.sidebar.show(SIDEBAR_ID);
+ await waitSidebarSelected;
+
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ // Test extension sidebar content.
+ ok(
+ sidebarPanelContent,
+ "Got a sidebar panel for the registered extension sidebar"
+ );
+
+ assertTreeView(sidebarPanelContent, {
+ expectedTreeTables: 1,
+ expectedStringCells: 2,
+ expectedNumberCells: 0,
+ });
+
+ // Test sidebar refreshed on further sidebar.setObject calls.
+ info("Change the inspected object in the extension sidebar object treeview");
+ sidebar.setObject({ aNewProperty: 123 });
+
+ assertTreeView(sidebarPanelContent, {
+ expectedTreeTables: 1,
+ expectedStringCells: 0,
+ expectedNumberCells: 1,
+ });
+});
+
+add_task(async function testSidebarSetExpressionResult() {
+ const { commands } = toolbox;
+ const sidebar = inspector.getPanel(SIDEBAR_ID);
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ info("Testing sidebar.setExpressionResult with rootTitle");
+
+ const expression = `
+ var obj = Object.create(null);
+ obj.prop1 = 123;
+ obj[Symbol('sym1')] = 456;
+ obj.cyclic = obj;
+ obj;
+ `;
+
+ const consoleFront = await toolbox.target.getFront("console");
+ let evalResult = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ expression,
+ {
+ consoleFront,
+ }
+ );
+
+ sidebar.setExpressionResult(evalResult, "Expected Root Title");
+
+ // Wait the ObjectInspector component to be rendered and test its content.
+ await testSetExpressionSidebarPanel(sidebarPanelContent, {
+ nodesLength: 4,
+ propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+ rootTitle: "Expected Root Title",
+ });
+
+ info("Testing sidebar.setExpressionResult without rootTitle");
+
+ sidebar.setExpressionResult(evalResult);
+
+ // Wait the ObjectInspector component to be rendered and test its content.
+ await testSetExpressionSidebarPanel(sidebarPanelContent, {
+ nodesLength: 4,
+ propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+ });
+
+ info("Test expanding the object");
+ const oi = sidebarPanelContent.querySelector(".tree");
+ const cyclicNode = oi.querySelectorAll(".node")[1];
+ ok(cyclicNode.innerText.includes("cyclic"), "Found the expected node");
+ cyclicNode.click();
+
+ await TestUtils.waitForCondition(
+ () => oi.querySelectorAll(".node").length === 7,
+ "Wait for the 'cyclic' node to be expanded"
+ );
+
+ await TestUtils.waitForCondition(
+ () => oi.querySelector(".tree-node.focused"),
+ "Wait for the 'cyclic' node to be focused"
+ );
+ ok(
+ oi.querySelector(".tree-node.focused").innerText.includes("cyclic"),
+ "'cyclic' node is focused"
+ );
+
+ info("Test keyboard navigation");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", {}, oi.ownerDocument.defaultView);
+ await TestUtils.waitForCondition(
+ () => oi.querySelectorAll(".node").length === 4,
+ "Wait for the 'cyclic' node to be collapsed"
+ );
+ ok(
+ oi.querySelector(".tree-node.focused").innerText.includes("cyclic"),
+ "'cyclic' node is still focused"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, oi.ownerDocument.defaultView);
+ await TestUtils.waitForCondition(
+ () => oi.querySelectorAll(".tree-node")[2].classList.contains("focused"),
+ "Wait for the 'prop1' node to be focused"
+ );
+
+ ok(
+ oi.querySelector(".tree-node.focused").innerText.includes("prop1"),
+ "'prop1' node is focused"
+ );
+
+ info(
+ "Testing sidebar.setExpressionResult for an expression returning a longstring"
+ );
+ evalResult = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ `"ab ".repeat(10000)`,
+ {
+ consoleFront,
+ }
+ );
+ sidebar.setExpressionResult(evalResult);
+
+ await TestUtils.waitForCondition(() => {
+ const longStringEl = sidebarPanelContent.querySelector(
+ ".tree .objectBox-string"
+ );
+ return (
+ longStringEl && longStringEl.textContent.includes("ab ".repeat(10000))
+ );
+ }, "Wait for the longString to be render with its full text");
+ ok(true, "The longString is expanded and its full text is displayed");
+
+ info(
+ "Testing sidebar.setExpressionResult for an expression returning a primitive"
+ );
+ evalResult = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ `1 + 2`,
+ {
+ consoleFront,
+ }
+ );
+ sidebar.setExpressionResult(evalResult);
+ const numberEl = await TestUtils.waitForCondition(
+ () => sidebarPanelContent.querySelector(".objectBox-number"),
+ "Wait for the result number element to be rendered"
+ );
+ is(numberEl.textContent, "3", `The "1 + 2" expression was evaluated as "3"`);
+});
+
+add_task(async function testSidebarDOMNodeHighlighting() {
+ const { commands } = toolbox;
+ const sidebar = inspector.getPanel(SIDEBAR_ID);
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ const expression = "({ body: document.body })";
+
+ const consoleFront = await toolbox.target.getFront("console");
+ const evalResult = await commands.inspectedWindowCommand.eval(
+ fakeExtCallerInfo,
+ expression,
+ {
+ consoleFront,
+ }
+ );
+
+ sidebar.setExpressionResult(evalResult);
+
+ // Wait the DOM node to be rendered inside the component.
+ await waitForObjectInspector(sidebarPanelContent, "node");
+
+ // Wait for the object to be expanded so we only target the "body" property node, and
+ // not the root object element.
+ await TestUtils.waitForCondition(
+ () =>
+ sidebarPanelContent.querySelectorAll(".object-inspector .tree-node")
+ .length > 1
+ );
+
+ // Get and verify the DOMNode and the "open inspector"" icon
+ // rendered inside the ObjectInspector.
+
+ assertObjectInspector(sidebarPanelContent, {
+ expectedDOMNodes: 2,
+ expectedOpenInspectors: 2,
+ });
+
+ // Test highlight DOMNode on mouseover.
+ info("Highlight the node by moving the cursor on it");
+
+ const onNodeHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+
+ moveMouseOnObjectInspectorDOMNode(sidebarPanelContent);
+
+ const { nodeFront } = await onNodeHighlight;
+ is(nodeFront.displayName, "body", "The correct node was highlighted");
+
+ // Test unhighlight DOMNode on mousemove.
+ info("Unhighlight the node by moving away from the node");
+ const onNodeUnhighlight = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+
+ moveMouseOnPanelCenter(sidebarPanelContent);
+
+ await onNodeUnhighlight;
+ info("The node is no longer highlighted");
+});
+
+add_task(async function testSidebarDOMNodeOpenInspector() {
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ // Test DOMNode selected in the inspector when "open inspector"" icon clicked.
+ info("Unselect node in the inspector");
+ let onceNewNodeFront = inspector.selection.once("new-node-front");
+ inspector.selection.setNodeFront(null);
+ let nodeFront = await onceNewNodeFront;
+ is(nodeFront, null, "The inspector selection should have been unselected");
+
+ info(
+ "Select the ObjectInspector DOMNode in the inspector panel by clicking on it"
+ );
+
+ // In test mode, shown highlighters are not automatically hidden after a delay to
+ // prevent intermittent test failures from race conditions.
+ // Restore this behavior just for this test because it is explicitly checked.
+ const HIGHLIGHTER_AUTOHIDE_TIMER = inspector.HIGHLIGHTER_AUTOHIDE_TIMER;
+ inspector.HIGHLIGHTER_AUTOHIDE_TIMER = 1000;
+ registerCleanupFunction(() => {
+ // Restore the value to disable autohiding to not impact other tests.
+ inspector.HIGHLIGHTER_AUTOHIDE_TIMER = HIGHLIGHTER_AUTOHIDE_TIMER;
+ });
+
+ const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
+ getHighlighterTestHelpers(inspector);
+
+ // Once we click the open-inspector icon we expect a new node front to be selected
+ // and the node to have been highlighted and unhighlighted.
+ const onNodeHighlight = waitForHighlighterTypeShown(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ const onNodeUnhighlight = waitForHighlighterTypeHidden(
+ inspector.highlighters.TYPES.BOXMODEL
+ );
+ onceNewNodeFront = inspector.selection.once("new-node-front");
+
+ clickOpenInspectorIcon(sidebarPanelContent);
+
+ nodeFront = await onceNewNodeFront;
+ is(nodeFront.displayName, "body", "The correct node has been selected");
+ const { nodeFront: highlightedNodeFront } = await onNodeHighlight;
+ is(
+ highlightedNodeFront.displayName,
+ "body",
+ "The correct node was highlighted"
+ );
+
+ await onNodeUnhighlight;
+});
+
+add_task(async function testSidebarSetExtensionPage() {
+ const sidebar = inspector.getPanel(SIDEBAR_ID);
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ info("Testing sidebar.setExtensionPage");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_unsafe_parent_loads", true]],
+ });
+
+ const expectedURL =
+ "data:text/html,<!DOCTYPE html><html><body><h1>Extension Page";
+
+ sidebar.setExtensionPage(expectedURL);
+
+ await testSetExtensionPageSidebarPanel(sidebarPanelContent, expectedURL);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function teardownExtensionSidebar() {
+ info("Remove the sidebar instance");
+
+ toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID);
+
+ ok(
+ !inspector.sidebar.getTabPanel(SIDEBAR_ID),
+ "The rendered extension sidebar has been removed"
+ );
+
+ const inspectorStoreState = inspector.store.getState();
+
+ Assert.deepEqual(
+ inspectorStoreState.extensionsSidebar,
+ {},
+ "The extensions sidebar Redux store data has been cleared"
+ );
+
+ await extension.unload();
+
+ toolbox = null;
+ inspector = null;
+ extension = null;
+});
+
+add_task(async function testActiveTabOnNonExistingSidebar() {
+ // Set a fake non existing sidebar id in the activeSidebar pref,
+ // to simulate the scenario where an extension has installed a sidebar
+ // which has been saved in the preference but it doesn't exist anymore.
+ await SpecialPowers.pushPrefEnv({
+ set: [["devtools.inspector.activeSidebar", "unexisting-sidebar-id"]],
+ });
+
+ const res = await openInspectorForURL("about:blank");
+ inspector = res.inspector;
+ toolbox = res.toolbox;
+
+ const onceSidebarCreated = toolbox.once(
+ `extension-sidebar-created-${SIDEBAR_ID}`
+ );
+ toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, {
+ title: SIDEBAR_TITLE,
+ });
+
+ // Wait the extension sidebar to be created and then unregister it to force the tabbar
+ // to select a new one.
+ await onceSidebarCreated;
+ toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID);
+
+ is(
+ inspector.sidebar.getCurrentTabID(),
+ "layoutview",
+ "Got the expected inspector sidebar tab selected"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/devtools/client/inspector/extensions/test/head.js b/devtools/client/inspector/extensions/test/head.js
new file mode 100644
index 0000000000..17d7538904
--- /dev/null
+++ b/devtools/client/inspector/extensions/test/head.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+ this
+);
+
+// Import the inspector extensions test helpers (shared between the tests that live
+// in the current devtools test directory and the devtools sidebar tests that live
+// in browser/components/extensions/test/browser).
+Services.scriptloader.loadSubScript(
+ new URL("head_devtools_inspector_sidebar.js", gTestPath).href,
+ this
+);
diff --git a/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
new file mode 100644
index 0000000000..73467c3e31
--- /dev/null
+++ b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported getExtensionSidebarActors, expectNoSuchActorIDs,
+ waitForObjectInspector, testSetExpressionSidebarPanel, assertTreeView,
+ assertObjectInspector, moveMouseOnObjectInspectorDOMNode,
+ moveMouseOnPanelCenter, clickOpenInspectorIcon */
+
+"use strict";
+
+const ACCORDION_LABEL_SELECTOR = ".accordion-header-label";
+const ACCORDION_CONTENT_SELECTOR = ".accordion-content";
+
+// Retrieve the array of all the objectValueGrip actors from the
+// inspector extension sidebars state
+// (used in browser_ext_devtools_panels_elements_sidebar.js).
+function getExtensionSidebarActors(inspector) {
+ const state = inspector.store.getState();
+
+ const actors = [];
+
+ for (const sidebarId of Object.keys(state.extensionsSidebar)) {
+ const sidebarState = state.extensionsSidebar[sidebarId];
+
+ if (
+ sidebarState.viewMode === "object-value-grip-view" &&
+ sidebarState.objectValueGrip &&
+ sidebarState.objectValueGrip.actor
+ ) {
+ actors.push(sidebarState.objectValueGrip.actor);
+ }
+ }
+
+ return actors;
+}
+
+// Test that the specified objectValueGrip actors have been released
+// on the remote debugging server
+// (used in browser_ext_devtools_panels_elements_sidebar.js).
+async function expectNoSuchActorIDs(client, actors) {
+ info(`Test that all the objectValueGrip actors have been released`);
+ for (const actor of actors) {
+ await Assert.rejects(
+ client.request({ to: actor, type: "requestTypes" }),
+ err => err.message == `No such actor for ID: ${actor}`
+ );
+ }
+}
+
+function waitForObjectInspector(panelDoc, waitForNodeWithType = "object") {
+ const selector = `.object-inspector .objectBox-${waitForNodeWithType}`;
+ return TestUtils.waitForCondition(() => {
+ return !!panelDoc.querySelectorAll(selector).length;
+ }, `Wait for objectInspector's node type "${waitForNodeWithType}" to be loaded`);
+}
+
+// Helper function used inside the sidebar.setExtensionPage test case.
+async function testSetExtensionPageSidebarPanel(panelDoc, expectedURL) {
+ const selector = "iframe.inspector-extension-sidebar-page";
+ const iframesCount = await TestUtils.waitForCondition(() => {
+ return panelDoc.querySelectorAll(selector).length;
+ }, "Wait for the extension page iframe");
+
+ is(
+ iframesCount,
+ 1,
+ "Got the expected number of iframes in the extension panel"
+ );
+
+ const iframeWindow = panelDoc.querySelector(selector).contentWindow;
+ await TestUtils.waitForCondition(() => {
+ return iframeWindow.document.readyState === "complete";
+ }, "Wait for the extension page iframe to complete to load");
+
+ is(
+ iframeWindow.location.href,
+ expectedURL,
+ "Got the expected url in the extension panel iframe"
+ );
+}
+
+// Helper function used inside the sidebar.setObjectValueGrip test case.
+async function testSetExpressionSidebarPanel(panel, expected) {
+ const { nodesLength, propertiesNames, rootTitle } = expected;
+
+ await waitForObjectInspector(panel);
+
+ const objectInspectors = [...panel.querySelectorAll(".tree")];
+ is(
+ objectInspectors.length,
+ 1,
+ "There is the expected number of object inspectors"
+ );
+ const [objectInspector] = objectInspectors;
+
+ await TestUtils.waitForCondition(() => {
+ return objectInspector.querySelectorAll(".node").length >= nodesLength;
+ }, "Wait the objectInspector to have been fully rendered");
+
+ const oiNodes = objectInspector.querySelectorAll(".node");
+
+ is(
+ oiNodes.length,
+ nodesLength,
+ "Got the expected number of nodes in the tree"
+ );
+ const propertiesNodes = [
+ ...objectInspector.querySelectorAll(".object-label"),
+ ].map(el => el.textContent);
+ is(
+ JSON.stringify(propertiesNodes),
+ JSON.stringify(propertiesNames),
+ "Got the expected property names"
+ );
+
+ if (rootTitle) {
+ // Also check that the ObjectInspector is rendered inside
+ // an Accordion component with the expected title.
+ const accordion = panel.querySelector(".accordion");
+
+ ok(accordion, "Got an Accordion component as expected");
+
+ is(
+ accordion.querySelector(ACCORDION_CONTENT_SELECTOR).firstChild,
+ objectInspector,
+ "The ObjectInspector should be inside the Accordion content"
+ );
+
+ is(
+ accordion.querySelector(ACCORDION_LABEL_SELECTOR).textContent,
+ rootTitle,
+ "The Accordion has the expected label"
+ );
+ } else {
+ // Also check that there is no Accordion component rendered
+ // inside the sidebar panel.
+ ok(
+ !panel.querySelector(".accordion"),
+ "Got no Accordion component as expected"
+ );
+ }
+}
+
+function assertTreeView(panelDoc, expectedContent) {
+ const { expectedTreeTables, expectedStringCells, expectedNumberCells } =
+ expectedContent;
+
+ if (expectedTreeTables) {
+ is(
+ panelDoc.querySelectorAll("table.treeTable").length,
+ expectedTreeTables,
+ "The panel document contains the expected number of TreeView components"
+ );
+ }
+
+ if (expectedStringCells) {
+ is(
+ panelDoc.querySelectorAll("table.treeTable .stringCell").length,
+ expectedStringCells,
+ "The panel document contains the expected number of string cells."
+ );
+ }
+
+ if (expectedNumberCells) {
+ is(
+ panelDoc.querySelectorAll("table.treeTable .numberCell").length,
+ expectedNumberCells,
+ "The panel document contains the expected number of number cells."
+ );
+ }
+}
+
+async function assertObjectInspector(panelDoc, expectedContent) {
+ const { expectedDOMNodes, expectedOpenInspectors } = expectedContent;
+
+ // Get and verify the DOMNode and the "open inspector"" icon
+ // rendered inside the ObjectInspector.
+ const nodes = panelDoc.querySelectorAll(".objectBox-node");
+ const nodeOpenInspectors = panelDoc.querySelectorAll(
+ ".objectBox-node .open-inspector"
+ );
+
+ is(
+ nodes.length,
+ expectedDOMNodes,
+ "Found the expected number of ObjectInspector DOMNodes"
+ );
+ is(
+ nodeOpenInspectors.length,
+ expectedOpenInspectors,
+ "Found the expected nuber of open-inspector icons inside the ObjectInspector"
+ );
+}
+
+function moveMouseOnObjectInspectorDOMNode(panelDoc, nodeIndex = 0) {
+ const nodes = panelDoc.querySelectorAll(".objectBox-node");
+ const node = nodes[nodeIndex];
+
+ ok(node, "Found the ObjectInspector DOMNode");
+
+ EventUtils.synthesizeMouseAtCenter(
+ node,
+ { type: "mousemove" },
+ node.ownerDocument.defaultView
+ );
+}
+
+function moveMouseOnPanelCenter(panelDoc) {
+ EventUtils.synthesizeMouseAtCenter(
+ panelDoc,
+ { type: "mousemove" },
+ panelDoc.window
+ );
+}
+
+function clickOpenInspectorIcon(panelDoc, nodeIndex = 0) {
+ const nodes = panelDoc.querySelectorAll(".objectBox-node .open-inspector");
+ const node = nodes[nodeIndex];
+
+ ok(node, "Found the ObjectInspector open-inspector icon");
+
+ node.click();
+}
diff --git a/devtools/client/inspector/extensions/types.js b/devtools/client/inspector/extensions/types.js
new file mode 100644
index 0000000000..187c281031
--- /dev/null
+++ b/devtools/client/inspector/extensions/types.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+// Helpers injected as props by extension-sidebar.js and used by the
+// ObjectInspector component (which is part of the ExpressionResultView).
+exports.serviceContainer = {
+ highlightDomElement: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ openNodeInInspector: PropTypes.func.isRequired,
+};