/* 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"); const expectedURL = getRootDirectory(gTestPath) + "extension_page.html"; sidebar.setExtensionPage(expectedURL); await testSetExtensionPageSidebarPanel(sidebarPanelContent, expectedURL); }); 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(); });