diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/inspector/extensions/test | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/extensions/test')
4 files changed, 709 insertions, 0 deletions
diff --git a/devtools/client/inspector/extensions/test/browser.ini b/devtools/client/inspector/extensions/test/browser.ini new file mode 100644 index 0000000000..a141e7afc6 --- /dev/null +++ b/devtools/client/inspector/extensions/test/browser.ini @@ -0,0 +1,13 @@ +[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] 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(); +} |