--- a/devtools/client/inspector/extensions/actions/index.js
+++ b/devtools/client/inspector/extensions/actions/index.js
@@ -6,12 +6,15 @@
const { createEnum } = require("devtools/client/shared/enum");
createEnum([
// Update the extension sidebar with an object TreeView.
"EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE",
+ // Update the extension sidebar with an object value grip preview.
+ "EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE",
+
// Remove an extension sidebar from the inspector store.
"EXTENSION_SIDEBAR_REMOVE"
], module.exports);
--- a/devtools/client/inspector/extensions/actions/sidebar.js
+++ b/devtools/client/inspector/extensions/actions/sidebar.js
@@ -1,16 +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";
const {
EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+ EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
EXTENSION_SIDEBAR_REMOVE,
} = require("./index");
module.exports = {
/**
* Update the sidebar with an object treeview.
*/
@@ -18,16 +19,28 @@ module.exports = {
return {
type: EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
sidebarId,
object,
};
},
/**
+ * Update the sidebar with an object actor preview.
+ */
+ updateObjectValueGripView(sidebarId, objectValueGrip, rootTitle) {
+ return {
+ type: EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
+ sidebarId,
+ objectValueGrip,
+ rootTitle,
+ };
+ },
+
+ /**
* Remove the extension sidebar from the inspector store.
*/
removeExtensionSidebar(sidebarId) {
return {
type: EXTENSION_SIDEBAR_REMOVE,
sidebarId,
};
}
--- a/devtools/client/inspector/extensions/components/ExtensionSidebar.js
+++ b/devtools/client/inspector/extensions/components/ExtensionSidebar.js
@@ -5,48 +5,67 @@
"use strict";
const { createFactory, PureComponent } = 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");
const ObjectTreeView = createFactory(require("./ObjectTreeView"));
+const ObjectValueGripView = createFactory(require("./ObjectValueGripView"));
+const Types = require("../types");
/**
* The ExtensionSidebar is a React component with 2 supported viewMode:
- * - an ObjectTreeView UI, used to show the JS objects (used by the sidebar.setObject
- * and sidebar.setExpression WebExtensions APIs)
- * - an ExtensionPage UI used to show an extension page (used by the sidebar.setPage
- * WebExtensions APIs).
+ * - an ObjectTreeView UI, used to show the JS objects
+ * (used by the sidebar.setObject WebExtensions APIs)
+ * - an ObjectValueGripView UI, used to show the objects value grips
+ * (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,
+ // Helpers injected as props by extension-sidebar.js.
+ serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired,
};
}
render() {
- const { id, extensionsSidebar } = this.props;
+ const {
+ id,
+ extensionsSidebar,
+ serviceContainer,
+ } = this.props;
let {
viewMode = "empty-sidebar",
- object
+ object,
+ objectValueGrip,
+ rootTitle
} = extensionsSidebar[id] || {};
let sidebarContentEl;
switch (viewMode) {
case "object-treeview":
sidebarContentEl = ObjectTreeView({ object });
break;
+ case "object-value-grip-view":
+ sidebarContentEl = ObjectValueGripView({
+ objectValueGrip,
+ serviceContainer,
+ rootTitle,
+ });
+ break;
case "empty-sidebar":
break;
default:
throw new Error(`Unknown ExtensionSidebar viewMode: "${viewMode}"`);
}
const className = "devtools-monospace extension-sidebar inspector-tabpanel";
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ObjectValueGripView.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const Accordion = createFactory(require("devtools/client/inspector/layout/components/Accordion"));
+const reps = require("devtools/client/shared/components/reps/reps");
+const Types = require("../types");
+
+const { REPS, MODE } = reps;
+const { Grip } = REPS;
+
+const ObjectInspector = createFactory(reps.ObjectInspector);
+
+class ObjectValueGripView extends PureComponent {
+ static get propTypes() {
+ return {
+ rootTitle: PropTypes.string,
+ objectValueGrip: 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 {
+ objectValueGrip,
+ serviceContainer,
+ rootTitle,
+ } = this.props;
+
+ 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: objectValueGrip && objectValueGrip.actor || JSON.stringify(objectValueGrip),
+ contents: {
+ value: objectValueGrip,
+ }
+ }],
+ createObjectClient: serviceContainer.createObjectClient,
+ releaseActor: serviceContainer.releaseActor,
+ // TODO: evaluate if there should also be a serviceContainer.openLink.
+ };
+
+ if (objectValueGrip && objectValueGrip.actor) {
+ 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,
+ opened: true,
+ },
+ ],
+ });
+ }
+
+ return ObjectInspector(objectInspectorProps);
+ }
+}
+
+module.exports = ObjectValueGripView;
--- a/devtools/client/inspector/extensions/components/moz.build
+++ b/devtools/client/inspector/extensions/components/moz.build
@@ -2,9 +2,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(
'ExtensionSidebar.js',
'ObjectTreeView.js',
+ 'ObjectValueGripView.js',
)
--- a/devtools/client/inspector/extensions/extension-sidebar.js
+++ b/devtools/client/inspector/extensions/extension-sidebar.js
@@ -2,20 +2,22 @@
* License, v. 2.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("devtools/client/shared/vendor/react");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const ObjectClient = require("devtools/shared/client/object-client");
const ExtensionSidebarComponent = createFactory(require("./components/ExtensionSidebar"));
const {
updateObjectTreeView,
+ updateObjectValueGripView,
removeExtensionSidebar,
} = require("./actions/sidebar");
/**
* 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
@@ -47,16 +49,60 @@ class ExtensionSidebar {
get provider() {
if (!this._provider) {
this._provider = createElement(Provider, {
store: this.store,
key: this.id,
title: this.title,
}, ExtensionSidebarComponent({
id: this.id,
+ serviceContainer: {
+ createObjectClient: (object) => {
+ return new ObjectClient(this.inspector.toolbox.target.client, object);
+ },
+ releaseActor: (actor) => {
+ if (!actor) {
+ return;
+ }
+ this.inspector.toolbox.target.client.release(actor);
+ },
+ highlightDomElement: (grip, options = {}) => {
+ const { highlighterUtils } = this.inspector.toolbox;
+
+ if (!highlighterUtils) {
+ return null;
+ }
+
+ return highlighterUtils.highlightDomValueGrip(grip, options);
+ },
+ unHighlightDomElement: (forceHide = false) => {
+ const { highlighterUtils } = this.inspector.toolbox;
+
+ if (!highlighterUtils) {
+ return null;
+ }
+
+ return highlighterUtils.unhighlight(forceHide);
+ },
+ openNodeInInspector: async (grip) => {
+ const { highlighterUtils } = this.inspector.toolbox;
+
+ if (!highlighterUtils) {
+ return null;
+ }
+
+ let front = await highlighterUtils.gripToNodeFront(grip);
+ let onInspectorUpdated = this.inspector.once("inspector-updated");
+ let onNodeFrontSet = this.inspector.toolbox.selection.setNodeFront(
+ front, "inspector-extension-sidebar"
+ );
+
+ return Promise.all([onNodeFrontSet, onInspectorUpdated]);
+ }
+ },
}));
}
return this._provider;
}
/**
* Destroy the ExtensionSidebar instance, dispatch a removeExtensionSidebar Redux action
@@ -88,11 +134,24 @@ class ExtensionSidebar {
*/
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.
+ */
+ setObjectValueGrip(objectValueGrip, rootTitle) {
+ if (this.removed) {
+ throw new Error("Unable to set an object preview on a removed ExtensionSidebar");
+ }
+
+ this.store.dispatch(updateObjectValueGripView(this.id, objectValueGrip, rootTitle));
+ }
}
module.exports = ExtensionSidebar;
--- a/devtools/client/inspector/extensions/moz.build
+++ b/devtools/client/inspector/extensions/moz.build
@@ -7,11 +7,12 @@
DIRS += [
'actions',
'components',
'reducers',
]
DevToolsModules(
'extension-sidebar.js',
+ 'types.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/extensions/reducers/sidebar.js
+++ b/devtools/client/inspector/extensions/reducers/sidebar.js
@@ -1,16 +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";
const {
EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+ EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
EXTENSION_SIDEBAR_REMOVE,
} = require("../actions/index");
const INITIAL_SIDEBAR = {};
let reducers = {
[EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE](sidebar, {sidebarId, object}) {
@@ -19,16 +20,30 @@ let reducers = {
return Object.assign({}, sidebar, {
[sidebarId]: {
viewMode: "object-treeview",
object,
}
});
},
+ [EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE](
+ sidebar, {sidebarId, objectValueGrip, rootTitle}
+ ) {
+ // Update the sidebar to a "object-treeview" which shows
+ // the passed object.
+ return Object.assign({}, sidebar, {
+ [sidebarId]: {
+ viewMode: "object-value-grip-view",
+ objectValueGrip,
+ rootTitle,
+ }
+ });
+ },
+
[EXTENSION_SIDEBAR_REMOVE](sidebar, {sidebarId}) {
// Remove the sidebar from the Redux store.
delete sidebar[sidebarId];
return Object.assign({}, sidebar);
},
};
--- a/devtools/client/inspector/extensions/test/browser.ini
+++ b/devtools/client/inspector/extensions/test/browser.ini
@@ -1,13 +1,14 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
head.js
+ head_devtools_inspector_sidebar.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/test-actor.js
!/devtools/client/shared/test/test-actor-registry.js
[browser_inspector_extension_sidebar.js]
\ No newline at end of file
--- a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -1,95 +1,241 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-add_task(async function () {
- const {inspector} = await openInspectorForURL("about:blank");
- const {toolbox} = inspector;
+ChromeUtils.defineModuleGetter(this, "ContentTaskUtils",
+ "resource://testing-common/ContentTaskUtils.jsm");
- const sidebarId = "an-extension-sidebar";
- const sidebarTitle = "Sidebar Title";
+loader.lazyGetter(this, "WebExtensionInspectedWindowFront", () => {
+ return require(
+ "devtools/shared/fronts/webextension-inspected-window"
+ ).WebExtensionInspectedWindowFront;
+}, true);
- const waitSidebarCreated = toolbox.once(`extension-sidebar-created-${sidebarId}`);
+const FAKE_CALLER_INFO = {
+ url: "moz-extension://fake-webextension-uuid/fake-caller-script.js",
+ lineNumber: 1,
+ addonId: "fake-webextension-uuid",
+};
+const SIDEBAR_ID = "an-extension-sidebar";
+const SIDEBAR_TITLE = "Sidebar Title";
- toolbox.registerInspectorExtensionSidebar(sidebarId, {title: sidebarTitle});
+let toolbox;
+let inspector;
- const sidebar = await waitSidebarCreated;
+add_task(async function setupExtensionSidebar() {
+ const res = await openInspectorForURL("about:blank");
+ inspector = res.inspector;
+ toolbox = res.toolbox;
- is(sidebar, inspector.getPanel(sidebarId),
- "Got an extension sidebar instance equal to the one saved in the inspector");
+ const onceSidebarCreated = toolbox.once(`extension-sidebar-created-${SIDEBAR_ID}`);
+ toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, {title: SIDEBAR_TITLE});
+
+ const sidebar = await onceSidebarCreated;
- is(sidebar.title, sidebarTitle,
+ // 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, sidebarTitle,
+ is(sidebar.provider.props.title, SIDEBAR_TITLE,
"Got the expeted title in the provider props");
+ // Test sidebar Redux state.
let 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() {
let object = {
propertyName: {
nestedProperty: "propertyValue",
anotherProperty: "anotherValue",
},
};
+ let sidebar = inspector.getPanel(SIDEBAR_ID);
sidebar.setObject(object);
- inspectorStoreState = inspector.store.getState();
-
+ // Test updated sidebar Redux state.
+ let 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, {
- [sidebarId]: {
+ [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(sidebarId);
-
+ inspector.sidebar.show(SIDEBAR_ID);
await waitSidebarSelected;
- const sidebarPanelContent = inspector.sidebar.getTabPanel(sidebarId);
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+ // Test extension sidebar content.
ok(sidebarPanelContent, "Got a sidebar panel for the registered extension sidebar");
- is(sidebarPanelContent.querySelectorAll("table.treeTable").length, 1,
- "The sidebar panel contains a rendered TreeView component");
+ assertTreeView(sidebarPanelContent, {
+ expectedTreeTables: 1,
+ expectedStringCells: 2,
+ expectedNumberCells: 0,
+ });
- is(sidebarPanelContent.querySelectorAll("table.treeTable .stringCell").length, 2,
- "The TreeView component contains the expected number of string cells.");
-
+ // Test sidebar refreshed on further sidebar.setObject calls.
info("Change the inspected object in the extension sidebar object treeview");
sidebar.setObject({aNewProperty: 123});
- is(sidebarPanelContent.querySelectorAll("table.treeTable .stringCell").length, 0,
- "The TreeView component doesn't contains any string cells anymore.");
+ assertTreeView(sidebarPanelContent, {
+ expectedTreeTables: 1,
+ expectedStringCells: 0,
+ expectedNumberCells: 1,
+ });
+});
+
+add_task(async function testSidebarSetObjectValueGrip() {
+ const inspectedWindowFront = new WebExtensionInspectedWindowFront(
+ toolbox.target.client, toolbox.target.form
+ );
+
+ const sidebar = inspector.getPanel(SIDEBAR_ID);
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ info("Testing sidebar.setObjectValueGrip with rootTitle");
+
+ let expression = `
+ var obj = Object.create(null);
+ obj.prop1 = 123;
+ obj[Symbol('sym1')] = 456;
+ obj.cyclic = obj;
+ obj;
+ `;
+
+ let evalResult = await inspectedWindowFront.eval(FAKE_CALLER_INFO, expression, {
+ evalResultAsGrip: true,
+ toolboxConsoleActorID: toolbox.target.form.consoleActor
+ });
+
+ sidebar.setObjectValueGrip(evalResult.valueGrip, "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.setObjectValueGrip without rootTitle");
+
+ sidebar.setObjectValueGrip(evalResult.valueGrip);
+
+ // Wait the ObjectInspector component to be rendered and test its content.
+ await testSetExpressionSidebarPanel(sidebarPanelContent, {
+ nodesLength: 4,
+ propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+ });
+
+ inspectedWindowFront.destroy();
+});
+
+add_task(async function testSidebarDOMNodeHighlighting() {
+ const inspectedWindowFront = new WebExtensionInspectedWindowFront(
+ toolbox.target.client, toolbox.target.form
+ );
+
+ const sidebar = inspector.getPanel(SIDEBAR_ID);
+ const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+ let expression = "({ body: document.body })";
- is(sidebarPanelContent.querySelectorAll("table.treeTable .numberCell").length, 1,
- "The TreeView component contains one number cells.");
+ let evalResult = await inspectedWindowFront.eval(FAKE_CALLER_INFO, expression, {
+ evalResultAsGrip: true,
+ toolboxConsoleActorID: toolbox.target.form.consoleActor
+ });
+
+ sidebar.setObjectValueGrip(evalResult.valueGrip);
+
+ // Wait the DOM node to be rendered inside the component.
+ await waitForObjectInspector(sidebarPanelContent, "node");
+
+ // Get and verify the DOMNode and the "open inspector"" icon
+ // rendered inside the ObjectInspector.
+ assertObjectInspector(sidebarPanelContent, {
+ expectedDOMNodes: 1,
+ expectedOpenInspectors: 1,
+ });
+
+ // Test highlight DOMNode on mouseover.
+ info("Highlight the node by moving the cursor on it");
+
+ let onNodeHighlight = toolbox.once("node-highlight");
+
+ moveMouseOnObjectInspectorDOMNode(sidebarPanelContent);
+
+ let 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");
+ let onNodeUnhighlight = toolbox.once("node-unhighlight");
+
+ moveMouseOnPanelCenter(sidebarPanelContent);
+ await onNodeUnhighlight;
+ info("node-unhighlight event was fired when moving away from the node");
+
+ inspectedWindowFront.destroy();
+});
+
+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, undefined, "The inspector selection should have been unselected");
+
+ info("Select the ObjectInspector DOMNode in the inspector panel by clicking on it");
+
+ // 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.
+ let onNodeHighlight = toolbox.once("node-highlight");
+ let onNodeUnhighlight = toolbox.once("node-unhighlight");
+ onceNewNodeFront = inspector.selection.once("new-node-front");
+
+ clickOpenInspectorIcon(sidebarPanelContent);
+
+ nodeFront = await onceNewNodeFront;
+ is(nodeFront.displayName, "body", "The correct node has been selected");
+ nodeFront = await onNodeHighlight;
+ is(nodeFront.displayName, "body", "The correct node was highlighted");
+
+ await onNodeUnhighlight;
+});
+
+add_task(async function teardownExtensionSidebar() {
info("Remove the sidebar instance");
- toolbox.unregisterInspectorExtensionSidebar(sidebarId);
+ toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID);
- ok(!inspector.sidebar.getTabPanel(sidebarId),
+ ok(!inspector.sidebar.getTabPanel(SIDEBAR_ID),
"The rendered extension sidebar has been removed");
- inspectorStoreState = inspector.store.getState();
+ let inspectorStoreState = inspector.store.getState();
Assert.deepEqual(inspectorStoreState.extensionsSidebar, {},
"The extensions sidebar Redux store data has been cleared");
await toolbox.destroy();
+
+ toolbox = null;
+ inspector = null;
});
--- a/devtools/client/inspector/extensions/test/head.js
+++ b/devtools/client/inspector/extensions/test/head.js
@@ -7,8 +7,15 @@
/* import-globals-from ../../test/head.js */
"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);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
@@ -0,0 +1,160 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported getExtensionSidebarActors, expectNoSuchActorIDs,
+ waitForObjectInspector, testSetExpressionSidebarPanel, assertTreeView,
+ assertObjectInspector, moveMouseOnObjectInspectorDOMNode,
+ moveMouseOnPanelCenter, clickOpenInspectorIcon */
+
+"use strict";
+
+// 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 (let 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 (let actor of actors) {
+ await Assert.rejects(client.request({to: actor, type: "requestTypes"}),
+ `No such actor for ID: ${actor}`);
+ }
+}
+
+function waitForObjectInspector(panelDoc, waitForNodeWithType = "object") {
+ const selector = `.object-inspector .objectBox-${waitForNodeWithType}`;
+ return ContentTaskUtils.waitForCondition(() => {
+ return panelDoc.querySelectorAll(selector).length > 0;
+ });
+}
+
+// Helper function used inside the sidebar.setObjectValueGrip test case.
+async function testSetExpressionSidebarPanel(panel, expected) {
+ const {
+ nodesLength,
+ propertiesNames,
+ rootTitle,
+ } = expected;
+
+ await waitForObjectInspector(panel);
+
+ let objectInspectors = [...panel.querySelectorAll(".tree")];
+ is(objectInspectors.length, 1, "There is the expected number of object inspectors");
+ let [objectInspector] = objectInspectors;
+
+ // Wait the objectInspector to have been fully rendered.
+ await ContentTaskUtils.waitForCondition(() => {
+ return objectInspector.querySelectorAll(".node").length >= nodesLength;
+ });
+
+ let oiNodes = objectInspector.querySelectorAll(".node");
+
+ is(oiNodes.length, nodesLength, "Got the expected number of nodes in the tree");
+ let 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("._content").firstChild, objectInspector,
+ "The ObjectInspector should be inside the Accordion content");
+
+ is(accordion.querySelector("._header").textContent.trim(), 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.
+ let nodes = panelDoc.querySelectorAll(".objectBox-node");
+ let 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) {
+ let nodes = panelDoc.querySelectorAll(".objectBox-node");
+ let 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) {
+ let nodes = panelDoc.querySelectorAll(".objectBox-node .open-inspector");
+ let node = nodes[nodeIndex];
+
+ ok(node, "Found the ObjectInspector open-inspector icon");
+
+ node.click();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/types.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";
+
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+// Helpers injected as props by extension-sidebar.js and used by the
+// ObjectInspector component (which is part of the ObjectValueGripView).
+exports.serviceContainer = {
+ createObjectClient: PropTypes.func.isRequired,
+ releaseActor: PropTypes.func.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ openNodeInInspector: PropTypes.func.isRequired,
+};