browser_inspector_extension_sidebar.js (13292B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const SIDEBAR_ID = "an-extension-sidebar"; 7 const SIDEBAR_TITLE = "Sidebar Title"; 8 9 let extension; 10 let fakeExtCallerInfo; 11 12 let toolbox; 13 let inspector; 14 15 add_task(async function setupExtensionSidebar() { 16 extension = ExtensionTestUtils.loadExtension({ 17 background() { 18 // This is just an empty extension used to ensure that the caller extension uuid 19 // actually exists. 20 }, 21 }); 22 23 await extension.startup(); 24 25 fakeExtCallerInfo = { 26 url: WebExtensionPolicy.getByID(extension.id).getURL( 27 "fake-caller-script.js" 28 ), 29 lineNumber: 1, 30 addonId: extension.id, 31 }; 32 33 const res = await openInspectorForURL("about:blank"); 34 inspector = res.inspector; 35 toolbox = res.toolbox; 36 37 const onceSidebarCreated = toolbox.once( 38 `extension-sidebar-created-${SIDEBAR_ID}` 39 ); 40 toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, { 41 title: SIDEBAR_TITLE, 42 }); 43 44 const sidebar = await onceSidebarCreated; 45 46 // Test sidebar properties. 47 is( 48 sidebar, 49 inspector.getPanel(SIDEBAR_ID), 50 "Got an extension sidebar instance equal to the one saved in the inspector" 51 ); 52 is( 53 sidebar.title, 54 SIDEBAR_TITLE, 55 "Got the expected title in the extension sidebar instance" 56 ); 57 is( 58 sidebar.provider.props.title, 59 SIDEBAR_TITLE, 60 "Got the expeted title in the provider props" 61 ); 62 63 // Test sidebar Redux state. 64 const inspectorStoreState = inspector.store.getState(); 65 ok( 66 "extensionsSidebar" in inspectorStoreState, 67 "Got the extensionsSidebar sub-state in the inspector Redux store" 68 ); 69 Assert.deepEqual( 70 inspectorStoreState.extensionsSidebar, 71 {}, 72 "The extensionsSidebar should be initially empty" 73 ); 74 }); 75 76 add_task(async function testSidebarSetObject() { 77 const object = { 78 propertyName: { 79 nestedProperty: "propertyValue", 80 anotherProperty: "anotherValue", 81 }, 82 }; 83 84 const sidebar = inspector.getPanel(SIDEBAR_ID); 85 sidebar.setObject(object); 86 87 // Test updated sidebar Redux state. 88 const inspectorStoreState = inspector.store.getState(); 89 is( 90 Object.keys(inspectorStoreState.extensionsSidebar).length, 91 1, 92 "The extensionsSidebar state contains the newly registered extension sidebar state" 93 ); 94 Assert.deepEqual( 95 inspectorStoreState.extensionsSidebar, 96 { 97 [SIDEBAR_ID]: { 98 viewMode: "object-treeview", 99 object, 100 }, 101 }, 102 "Got the expected state for the registered extension sidebar" 103 ); 104 105 // Select the extension sidebar. 106 const waitSidebarSelected = toolbox.once(`inspector-sidebar-select`); 107 inspector.sidebar.show(SIDEBAR_ID); 108 await waitSidebarSelected; 109 110 const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); 111 112 // Test extension sidebar content. 113 ok( 114 sidebarPanelContent, 115 "Got a sidebar panel for the registered extension sidebar" 116 ); 117 118 assertTreeView(sidebarPanelContent, { 119 expectedTreeTables: 1, 120 expectedStringCells: 2, 121 expectedNumberCells: 0, 122 }); 123 124 // Test sidebar refreshed on further sidebar.setObject calls. 125 info("Change the inspected object in the extension sidebar object treeview"); 126 sidebar.setObject({ aNewProperty: 123 }); 127 128 assertTreeView(sidebarPanelContent, { 129 expectedTreeTables: 1, 130 expectedStringCells: 0, 131 expectedNumberCells: 1, 132 }); 133 }); 134 135 add_task(async function testSidebarSetExpressionResult() { 136 const { commands } = toolbox; 137 const sidebar = inspector.getPanel(SIDEBAR_ID); 138 const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); 139 140 info("Testing sidebar.setExpressionResult with rootTitle"); 141 142 const expression = ` 143 var obj = Object.create(null); 144 obj.prop1 = 123; 145 obj[Symbol('sym1')] = 456; 146 obj.cyclic = obj; 147 obj; 148 `; 149 150 const consoleFront = await toolbox.target.getFront("console"); 151 let evalResult = await commands.inspectedWindowCommand.eval( 152 fakeExtCallerInfo, 153 expression, 154 { 155 consoleFront, 156 } 157 ); 158 159 sidebar.setExpressionResult(evalResult, "Expected Root Title"); 160 161 // Wait the ObjectInspector component to be rendered and test its content. 162 await testSetExpressionSidebarPanel(sidebarPanelContent, { 163 nodesLength: 4, 164 propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"], 165 rootTitle: "Expected Root Title", 166 }); 167 168 info("Testing sidebar.setExpressionResult without rootTitle"); 169 170 sidebar.setExpressionResult(evalResult); 171 172 // Wait the ObjectInspector component to be rendered and test its content. 173 await testSetExpressionSidebarPanel(sidebarPanelContent, { 174 nodesLength: 4, 175 propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"], 176 }); 177 178 info("Test expanding the object"); 179 const oi = sidebarPanelContent.querySelector(".tree"); 180 const cyclicNode = oi.querySelectorAll(".node")[1]; 181 ok(cyclicNode.innerText.includes("cyclic"), "Found the expected node"); 182 cyclicNode.click(); 183 184 await TestUtils.waitForCondition( 185 () => oi.querySelectorAll(".node").length === 7, 186 "Wait for the 'cyclic' node to be expanded" 187 ); 188 189 await TestUtils.waitForCondition( 190 () => oi.querySelector(".tree-node.focused"), 191 "Wait for the 'cyclic' node to be focused" 192 ); 193 ok( 194 oi.querySelector(".tree-node.focused").innerText.includes("cyclic"), 195 "'cyclic' node is focused" 196 ); 197 198 info("Test keyboard navigation"); 199 EventUtils.synthesizeKey("KEY_ArrowLeft", {}, oi.ownerDocument.defaultView); 200 await TestUtils.waitForCondition( 201 () => oi.querySelectorAll(".node").length === 4, 202 "Wait for the 'cyclic' node to be collapsed" 203 ); 204 ok( 205 oi.querySelector(".tree-node.focused").innerText.includes("cyclic"), 206 "'cyclic' node is still focused" 207 ); 208 209 EventUtils.synthesizeKey("KEY_ArrowDown", {}, oi.ownerDocument.defaultView); 210 await TestUtils.waitForCondition( 211 () => oi.querySelectorAll(".tree-node")[2].classList.contains("focused"), 212 "Wait for the 'prop1' node to be focused" 213 ); 214 215 ok( 216 oi.querySelector(".tree-node.focused").innerText.includes("prop1"), 217 "'prop1' node is focused" 218 ); 219 220 info( 221 "Testing sidebar.setExpressionResult for an expression returning a longstring" 222 ); 223 evalResult = await commands.inspectedWindowCommand.eval( 224 fakeExtCallerInfo, 225 `"ab ".repeat(10000)`, 226 { 227 consoleFront, 228 } 229 ); 230 sidebar.setExpressionResult(evalResult); 231 232 await TestUtils.waitForCondition(() => { 233 const longStringEl = sidebarPanelContent.querySelector( 234 ".tree .objectBox-string" 235 ); 236 return ( 237 longStringEl && longStringEl.textContent.includes("ab ".repeat(10000)) 238 ); 239 }, "Wait for the longString to be render with its full text"); 240 ok(true, "The longString is expanded and its full text is displayed"); 241 242 info( 243 "Testing sidebar.setExpressionResult for an expression returning a primitive" 244 ); 245 evalResult = await commands.inspectedWindowCommand.eval( 246 fakeExtCallerInfo, 247 `1 + 2`, 248 { 249 consoleFront, 250 } 251 ); 252 sidebar.setExpressionResult(evalResult); 253 const numberEl = await TestUtils.waitForCondition( 254 () => sidebarPanelContent.querySelector(".objectBox-number"), 255 "Wait for the result number element to be rendered" 256 ); 257 is(numberEl.textContent, "3", `The "1 + 2" expression was evaluated as "3"`); 258 }); 259 260 add_task(async function testSidebarDOMNodeHighlighting() { 261 const { commands } = toolbox; 262 const sidebar = inspector.getPanel(SIDEBAR_ID); 263 const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); 264 265 const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = 266 getHighlighterTestHelpers(inspector); 267 268 const expression = "({ body: document.body })"; 269 270 const consoleFront = await toolbox.target.getFront("console"); 271 const evalResult = await commands.inspectedWindowCommand.eval( 272 fakeExtCallerInfo, 273 expression, 274 { 275 consoleFront, 276 } 277 ); 278 279 sidebar.setExpressionResult(evalResult); 280 281 // Wait the DOM node to be rendered inside the component. 282 await waitForObjectInspector(sidebarPanelContent, "node"); 283 284 // Wait for the object to be expanded so we only target the "body" property node, and 285 // not the root object element. 286 await TestUtils.waitForCondition( 287 () => 288 sidebarPanelContent.querySelectorAll(".object-inspector .tree-node") 289 .length > 1 290 ); 291 292 // Get and verify the DOMNode and the "open inspector"" icon 293 // rendered inside the ObjectInspector. 294 295 assertObjectInspector(sidebarPanelContent, { 296 expectedDOMNodes: 2, 297 expectedOpenInspectors: 2, 298 }); 299 300 // Test highlight DOMNode on mouseover. 301 info("Highlight the node by moving the cursor on it"); 302 303 const onNodeHighlight = waitForHighlighterTypeShown( 304 inspector.highlighters.TYPES.BOXMODEL 305 ); 306 307 moveMouseOnObjectInspectorDOMNode(sidebarPanelContent); 308 309 const { nodeFront } = await onNodeHighlight; 310 is(nodeFront.displayName, "body", "The correct node was highlighted"); 311 312 // Test unhighlight DOMNode on mousemove. 313 info("Unhighlight the node by moving away from the node"); 314 const onNodeUnhighlight = waitForHighlighterTypeHidden( 315 inspector.highlighters.TYPES.BOXMODEL 316 ); 317 318 moveMouseOnPanelCenter(sidebarPanelContent); 319 320 await onNodeUnhighlight; 321 info("The node is no longer highlighted"); 322 }); 323 324 add_task(async function testSidebarDOMNodeOpenInspector() { 325 const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); 326 327 // Test DOMNode selected in the inspector when "open inspector"" icon clicked. 328 info("Unselect node in the inspector"); 329 let onceNewNodeFront = inspector.selection.once("new-node-front"); 330 inspector.selection.setNodeFront(null); 331 let nodeFront = await onceNewNodeFront; 332 is(nodeFront, null, "The inspector selection should have been unselected"); 333 334 info( 335 "Select the ObjectInspector DOMNode in the inspector panel by clicking on it" 336 ); 337 338 // In test mode, shown highlighters are not automatically hidden after a delay to 339 // prevent intermittent test failures from race conditions. 340 // Restore this behavior just for this test because it is explicitly checked. 341 const HIGHLIGHTER_AUTOHIDE_TIMER = inspector.HIGHLIGHTER_AUTOHIDE_TIMER; 342 inspector.HIGHLIGHTER_AUTOHIDE_TIMER = 1000; 343 registerCleanupFunction(() => { 344 // Restore the value to disable autohiding to not impact other tests. 345 inspector.HIGHLIGHTER_AUTOHIDE_TIMER = HIGHLIGHTER_AUTOHIDE_TIMER; 346 }); 347 348 const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } = 349 getHighlighterTestHelpers(inspector); 350 351 // Once we click the open-inspector icon we expect a new node front to be selected 352 // and the node to have been highlighted and unhighlighted. 353 const onNodeHighlight = waitForHighlighterTypeShown( 354 inspector.highlighters.TYPES.BOXMODEL 355 ); 356 const onNodeUnhighlight = waitForHighlighterTypeHidden( 357 inspector.highlighters.TYPES.BOXMODEL 358 ); 359 onceNewNodeFront = inspector.selection.once("new-node-front"); 360 361 clickOpenInspectorIcon(sidebarPanelContent); 362 363 nodeFront = await onceNewNodeFront; 364 is(nodeFront.displayName, "body", "The correct node has been selected"); 365 const { nodeFront: highlightedNodeFront } = await onNodeHighlight; 366 is( 367 highlightedNodeFront.displayName, 368 "body", 369 "The correct node was highlighted" 370 ); 371 372 await onNodeUnhighlight; 373 }); 374 375 add_task(async function testSidebarSetExtensionPage() { 376 const sidebar = inspector.getPanel(SIDEBAR_ID); 377 const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID); 378 379 info("Testing sidebar.setExtensionPage"); 380 381 const expectedURL = getRootDirectory(gTestPath) + "extension_page.html"; 382 383 sidebar.setExtensionPage(expectedURL); 384 385 await testSetExtensionPageSidebarPanel(sidebarPanelContent, expectedURL); 386 }); 387 388 add_task(async function teardownExtensionSidebar() { 389 info("Remove the sidebar instance"); 390 391 toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID); 392 393 ok( 394 !inspector.sidebar.getTabPanel(SIDEBAR_ID), 395 "The rendered extension sidebar has been removed" 396 ); 397 398 const inspectorStoreState = inspector.store.getState(); 399 400 Assert.deepEqual( 401 inspectorStoreState.extensionsSidebar, 402 {}, 403 "The extensions sidebar Redux store data has been cleared" 404 ); 405 406 await extension.unload(); 407 408 toolbox = null; 409 inspector = null; 410 extension = null; 411 }); 412 413 add_task(async function testActiveTabOnNonExistingSidebar() { 414 // Set a fake non existing sidebar id in the activeSidebar pref, 415 // to simulate the scenario where an extension has installed a sidebar 416 // which has been saved in the preference but it doesn't exist anymore. 417 await SpecialPowers.pushPrefEnv({ 418 set: [["devtools.inspector.activeSidebar", "unexisting-sidebar-id"]], 419 }); 420 421 const res = await openInspectorForURL("about:blank"); 422 inspector = res.inspector; 423 toolbox = res.toolbox; 424 425 const onceSidebarCreated = toolbox.once( 426 `extension-sidebar-created-${SIDEBAR_ID}` 427 ); 428 toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, { 429 title: SIDEBAR_TITLE, 430 }); 431 432 // Wait the extension sidebar to be created and then unregister it to force the tabbar 433 // to select a new one. 434 await onceSidebarCreated; 435 toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID); 436 437 is( 438 inspector.sidebar.getCurrentTabID(), 439 "layoutview", 440 "Got the expected inspector sidebar tab selected" 441 ); 442 443 await SpecialPowers.popPrefEnv(); 444 });