browser_dbg-features-source-tree.js (18380B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 /** 6 * This test focuses on the SourceTree component, where we display all debuggable sources. 7 * 8 * The first two tests expand the tree via manual DOM events (first with clicks and second with keys). 9 * `waitForSourcesInSourceTree()` is a key assertion method. Passing `{noExpand: true}` 10 * is important to avoid automatically expand the source tree. 11 * 12 * The following tests depend on auto-expand and only assert all the sources possibly displayed 13 */ 14 15 "use strict"; 16 17 const testServer = createVersionizedHttpTestServer( 18 "../examples/sourcemaps-reload-uncompressed" 19 ); 20 const TEST_URL = testServer.urlFor("index.html"); 21 22 /** 23 * This test opens the SourceTree manually via click events on the nested source, 24 * and then adds a source dynamically and asserts it is visible. 25 */ 26 add_task(async function testSimpleSourcesWithManualClickExpand() { 27 const dbg = await initDebugger( 28 "doc-sources.html", 29 "simple1.js", 30 "simple2.js", 31 "nested-source.js", 32 "long.js" 33 ); 34 35 // Expand nodes and make sure more sources appear. 36 is( 37 getSourceTreeLabel(dbg, 1), 38 "Main Thread", 39 "Main thread is labeled properly" 40 ); 41 info("Before interacting with the source tree, no source are displayed"); 42 await waitForSourcesInSourceTree(dbg, [], { noExpand: true }); 43 await clickElement(dbg, "sourceDirectoryLabel", 3); 44 info( 45 "After clicking on the directory, all sources but the nested ones are displayed" 46 ); 47 await waitForSourcesInSourceTree( 48 dbg, 49 ["doc-sources.html", "simple1.js", "simple2.js", "long.js"], 50 { noExpand: true } 51 ); 52 53 await clickElement(dbg, "sourceDirectoryLabel", 4); 54 info( 55 "After clicking on the nested directory, the nested source is also displayed" 56 ); 57 await waitForSourcesInSourceTree( 58 dbg, 59 [ 60 "doc-sources.html", 61 "simple1.js", 62 "simple2.js", 63 "long.js", 64 "nested-source.js", 65 ], 66 { noExpand: true } 67 ); 68 69 const selected = waitForDispatch(dbg.store, "SET_SELECTED_LOCATION"); 70 await clickElement(dbg, "sourceNode", 5); 71 await selected; 72 await waitForSelectedSource(dbg, "nested-source.js"); 73 74 // Ensure the source file clicked is now focused 75 await waitForElementWithSelector(dbg, ".sources-list .focused"); 76 77 const selectedSource = dbg.selectors.getSelectedSource().url; 78 ok(selectedSource.includes("nested-source.js"), "nested-source is selected"); 79 await assertNodeIsFocused(dbg, 5); 80 81 // Make sure new sources appear in the list. 82 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 83 const script = content.document.createElement("script"); 84 script.src = "math.min.js"; 85 content.document.body.appendChild(script); 86 }); 87 88 info("After adding math.min.js, we got a new source displayed"); 89 await waitForSourcesInSourceTree( 90 dbg, 91 [ 92 "doc-sources.html", 93 "simple1.js", 94 "simple2.js", 95 "long.js", 96 "nested-source.js", 97 "math.min.js", 98 ], 99 { noExpand: true } 100 ); 101 is( 102 getSourceNodeLabel(dbg, 8), 103 "math.min.js", 104 "math.min.js - The dynamic script exists" 105 ); 106 107 info("Assert that nested-source.js is still the selected source"); 108 await assertNodeIsFocused(dbg, 5); 109 110 info("Test the copy to clipboard context menu"); 111 const mathMinTreeNode = findSourceNodeWithText(dbg, "math.min.js"); 112 await triggerSourceTreeContextMenu( 113 dbg, 114 mathMinTreeNode, 115 "#node-menu-copy-source" 116 ); 117 const clipboardData = SpecialPowers.getClipboardData("text/plain"); 118 is( 119 clipboardData, 120 EXAMPLE_URL + "math.min.js", 121 "The clipboard content is the selected source URL" 122 ); 123 124 info("Test the download file context menu"); 125 // Before trigerring the menu, mock the file picker 126 const MockFilePicker = SpecialPowers.MockFilePicker; 127 MockFilePicker.init(window.browsingContext); 128 const nsiFile = new FileUtils.File( 129 PathUtils.join(PathUtils.tempDir, `export_source_content_${Date.now()}.log`) 130 ); 131 MockFilePicker.setFiles([nsiFile]); 132 const path = nsiFile.path; 133 134 await triggerSourceTreeContextMenu( 135 dbg, 136 mathMinTreeNode, 137 "#node-menu-download-file" 138 ); 139 140 info("Wait for the downloaded file to be fully saved to disk"); 141 await BrowserTestUtils.waitForCondition(() => IOUtils.exists(path)); 142 await BrowserTestUtils.waitForCondition(async () => { 143 const { size } = await IOUtils.stat(path); 144 return size > 0; 145 }); 146 const buffer = await IOUtils.read(path); 147 const savedFileContent = new TextDecoder().decode(buffer); 148 149 const mathMinRequest = await fetch(EXAMPLE_URL + "math.min.js"); 150 const mathMinContent = await mathMinRequest.text(); 151 152 is( 153 savedFileContent, 154 mathMinContent, 155 "The downloaded file has the expected content" 156 ); 157 158 dbg.toolbox.closeToolbox(); 159 }); 160 161 /** 162 * Test keyboard arrow behaviour on the SourceTree with a nested folder 163 * that we manually expand/collapse via arrow keys. 164 */ 165 add_task(async function testSimpleSourcesWithManualKeyShortcutsExpand() { 166 const dbg = await initDebugger( 167 "doc-sources.html", 168 "simple1.js", 169 "simple2.js", 170 "nested-source.js", 171 "long.js" 172 ); 173 174 // Before clicking on the source label, no source is displayed 175 await waitForSourcesInSourceTree(dbg, [], { noExpand: true }); 176 await clickElement(dbg, "sourceDirectoryLabel", 3); 177 // Right after, all sources, but the nested one are displayed 178 await waitForSourcesInSourceTree( 179 dbg, 180 ["doc-sources.html", "simple1.js", "simple2.js", "long.js"], 181 { noExpand: true } 182 ); 183 184 // Right key on open dir 185 await pressKey(dbg, "Right"); 186 await assertNodeIsFocused(dbg, 3); 187 188 // Right key on closed dir 189 await pressKey(dbg, "Right"); 190 await assertNodeIsFocused(dbg, 4); 191 192 // Left key on a open dir 193 await pressKey(dbg, "Left"); 194 await assertNodeIsFocused(dbg, 4); 195 196 // Down key on a closed dir 197 await pressKey(dbg, "Down"); 198 await assertNodeIsFocused(dbg, 4); 199 200 // Right key on a source 201 // We are focused on the nested source and up to this point we still display only the 4 initial sources 202 await waitForSourcesInSourceTree( 203 dbg, 204 ["doc-sources.html", "simple1.js", "simple2.js", "long.js"], 205 { noExpand: true } 206 ); 207 await pressKey(dbg, "Right"); 208 await assertNodeIsFocused(dbg, 4); 209 // Now, the nested source is also displayed 210 await waitForSourcesInSourceTree( 211 dbg, 212 [ 213 "doc-sources.html", 214 "simple1.js", 215 "simple2.js", 216 "long.js", 217 "nested-source.js", 218 ], 219 { noExpand: true } 220 ); 221 222 // Down key on a source 223 await pressKey(dbg, "Down"); 224 await assertNodeIsFocused(dbg, 5); 225 226 // Go to bottom of tree and press down key 227 await pressKey(dbg, "Down"); 228 await pressKey(dbg, "Down"); 229 await assertNodeIsFocused(dbg, 6); 230 231 // Up key on a source 232 await pressKey(dbg, "Up"); 233 await assertNodeIsFocused(dbg, 5); 234 235 // Left key on a source 236 await pressKey(dbg, "Left"); 237 await assertNodeIsFocused(dbg, 4); 238 239 // Left key on a closed dir 240 // We are about to close the nested folder, the nested source is about to disappear 241 await waitForSourcesInSourceTree( 242 dbg, 243 [ 244 "doc-sources.html", 245 "simple1.js", 246 "simple2.js", 247 "long.js", 248 "nested-source.js", 249 ], 250 { noExpand: true } 251 ); 252 await pressKey(dbg, "Left"); 253 // And it disappeared 254 await waitForSourcesInSourceTree( 255 dbg, 256 ["doc-sources.html", "simple1.js", "simple2.js", "long.js"], 257 { noExpand: true } 258 ); 259 await pressKey(dbg, "Left"); 260 await assertNodeIsFocused(dbg, 3); 261 262 // Up Key at the top of the source tree 263 await pressKey(dbg, "Up"); 264 await assertNodeIsFocused(dbg, 2); 265 dbg.toolbox.closeToolbox(); 266 }); 267 268 /** 269 * Tests that the source tree works with all the various types of sources 270 * coming from the integration test page. 271 * 272 * Also assert a few extra things on sources with query strings: 273 * - they can be pretty printed, 274 * - quick open matches them, 275 * - you can set breakpoint on them. 276 */ 277 add_task(async function testSourceTreeOnTheIntegrationTestPage() { 278 // We open against a blank page and only then navigate to the test page 279 // so that sources aren't GC-ed before opening the debugger. 280 // When we (re)load a page while the debugger is opened, the debugger 281 // will force all sources to be held in memory. 282 const dbg = await initDebuggerWithAbsoluteURL("about:blank"); 283 284 await navigateToAbsoluteURL( 285 dbg, 286 TEST_URL, 287 "index.html", 288 "script.js", 289 "test-functions.js", 290 "query.js?x=1", 291 "query.js?x=2", 292 "query2.js?y=3", 293 "bundle.js", 294 "original.js", 295 "replaced-bundle.js", 296 "removed-original.js", 297 "named-eval.js" 298 ); 299 300 info("Verify source tree content"); 301 await waitForSourcesInSourceTree(dbg, INTEGRATION_TEST_PAGE_SOURCES); 302 303 info("Verify Thread Source Items"); 304 const mainThreadItem = findSourceTreeThreadByName(dbg, "Main Thread"); 305 ok(mainThreadItem, "Found the thread item for the main thread"); 306 ok( 307 mainThreadItem.querySelector("span.dbg-img-window"), 308 "The thread has the window icon" 309 ); 310 311 info( 312 "Assert the number of sources and source actors for the same-url.sjs sources" 313 ); 314 const sameUrlSource = findSource(dbg, "same-url.sjs"); 315 ok(sameUrlSource, "Found same-url.js in the main thread"); 316 317 const sourceActors = dbg.selectors.getSourceActorsForSource(sameUrlSource.id); 318 319 const mainThread = dbg.selectors 320 .getAllThreads() 321 .find(thread => thread.name == "Main Thread"); 322 323 const expectedSameUrlSources = 3; 324 is( 325 sourceActors.filter(actor => actor.thread == mainThread.actor).length, 326 expectedSameUrlSources, 327 `same-url.js is loaded ${expectedSameUrlSources} times in the main thread` 328 ); 329 330 const iframeThread = dbg.selectors 331 .getAllThreads() 332 .find(thread => thread.name == testServer.urlFor("iframe.html")); 333 334 is( 335 sourceActors.filter(actor => actor.thread == iframeThread.actor).length, 336 1, 337 "same-url.js is loaded one time in the iframe thread" 338 ); 339 340 const workerThread = dbg.selectors 341 .getAllThreads() 342 .find(thread => thread.url == testServer.urlFor("same-url.sjs")); 343 344 is( 345 sourceActors.filter(actor => actor.thread == workerThread.actor).length, 346 1, 347 "same-url.js is loaded one time in the worker thread" 348 ); 349 350 const workerThreadItem = findSourceTreeThreadByName(dbg, "same-url.sjs"); 351 ok(workerThreadItem, "Found the thread item for the worker"); 352 ok( 353 workerThreadItem.querySelector("span.dbg-img-worker"), 354 "The thread has the worker icon" 355 ); 356 357 info("Verify source icons"); 358 assertSourceIcon(dbg, "index.html", "file"); 359 assertSourceIcon(dbg, "script.js", "javascript"); 360 assertSourceIcon(dbg, "query.js?x=1", "javascript"); 361 assertSourceIcon(dbg, "original.js", "javascript"); 362 // Framework icons are only displayed when we parse the source, 363 // which happens when we select the source 364 assertSourceIcon(dbg, "react-component-module.js", "javascript"); 365 const onResumed = SpecialPowers.spawn( 366 gBrowser.selectedBrowser, 367 [], 368 function () { 369 content.eval("pauseInReact()"); 370 } 371 ); 372 await waitForPaused(dbg); 373 assertSourceIcon(dbg, "react-component-module.js", "javascript"); 374 await resume(dbg); 375 await onResumed; 376 377 info("Select an original source while bundle should be opened by default"); 378 // The setting should be ignored when selecting a source from the source tree 379 await dbg.actions.setDefaultSelectedLocation(false); 380 await selectSourceFromSourceTree(dbg, "original.js"); 381 assertTextContentOnLine(dbg, 1, `window.bar = function bar() {`); 382 383 info("Verify blackbox source icon"); 384 await selectSourceFromSourceTree(dbg, "script.js"); 385 await clickElement(dbg, "blackbox"); 386 await waitForDispatch(dbg.store, "BLACKBOX_WHOLE_SOURCES"); 387 assertSourceIcon(dbg, "script.js", "blackBox"); 388 await clickElement(dbg, "blackbox"); 389 await waitForDispatch(dbg.store, "UNBLACKBOX_WHOLE_SOURCES"); 390 assertSourceIcon(dbg, "script.js", "javascript"); 391 392 info("Assert the content of the named eval"); 393 await selectSourceFromSourceTree(dbg, "named-eval.js"); 394 assertTextContentOnLine(dbg, 3, `console.log("named-eval");`); 395 396 info("Assert that nameless eval don't show up in the source tree"); 397 invokeInTab("breakInEval"); 398 await waitForPaused(dbg); 399 await waitForSourcesInSourceTree(dbg, INTEGRATION_TEST_PAGE_SOURCES); 400 await resume(dbg); 401 402 info("Assert the content of sources with query string"); 403 await selectSourceFromSourceTree(dbg, "query.js?x=1"); 404 const tab = findElement(dbg, "activeTab"); 405 is(tab.innerText, "query.js?x=1", "Tab label is query.js?x=1"); 406 assertTextContentOnLine( 407 dbg, 408 1, 409 `function query() {console.log("query x=1");}` 410 ); 411 await addBreakpoint(dbg, "query.js?x=1", 1); 412 assertBreakpointHeading(dbg, "query.js?x=1", 0); 413 414 // pretty print the source and check the tab text 415 await togglePrettyPrint(dbg); 416 417 const prettyTab = findElement(dbg, "activeTab"); 418 is(prettyTab.innerText, "query.js?x=1", "Tab label is query.js?x=1"); 419 assertBreakpointHeading(dbg, "query.js?x=1", 0); 420 assertTextContentOnLine(dbg, 1, `function query() {`); 421 // Note the replacements of " by ' here: 422 assertTextContentOnLine(dbg, 2, `console.log('query x=1');`); 423 424 // assert quick open works with queries 425 pressKey(dbg, "quickOpen"); 426 type(dbg, "query.js?x"); 427 428 // There can be intermediate updates in the results, 429 // so wait for the final expected value 430 await waitFor(async () => { 431 const resultItem = findElement(dbg, "resultItems"); 432 if (!resultItem) { 433 return false; 434 } 435 return resultItem.innerText.includes("query.js?x=1"); 436 }, "Results include the source with the query string"); 437 dbg.toolbox.closeToolbox(); 438 }); 439 440 /** 441 * Verify that Web Extension content scripts appear only when 442 * devtools.chrome.enabled is set to true and that they get 443 * automatically re-selected on page reload. 444 */ 445 add_task(async function testSourceTreeWithWebExtensionContentScript() { 446 const extension = await installAndStartContentScriptExtension(); 447 448 // Ensure that the setting to show content script is off before running the test 449 await pushPref("devtools.debugger.show-content-scripts", false); 450 let dbg = await initDebugger("doc-content-script-sources.html"); 451 // Let some time for unexpected source to appear 452 await wait(1000); 453 // There is no content script, but still html pages inline sources 454 await waitForSourcesInSourceTree(dbg, [ 455 "doc-content-script-sources.html", 456 "doc-strict.html", 457 ]); 458 await dbg.toolbox.closeToolbox(); 459 460 const toolbox = await openToolboxForTab(gBrowser.selectedTab, "jsdebugger"); 461 dbg = createDebuggerContext(toolbox); 462 463 info("Enable the content script setting"); 464 await toggleSourcesTreeSettingsMenuItem(dbg, { 465 className: ".debugger-settings-menu-item-show-content-scripts", 466 isChecked: false, 467 }); 468 469 await waitForSourcesInSourceTree(dbg, [ 470 "doc-content-script-sources.html", 471 "doc-strict.html", 472 "content_script.js", 473 ]); 474 await selectSourceFromSourceTree(dbg, "content_script.js"); 475 ok( 476 findElementWithSelector(dbg, ".sources-list .focused"), 477 "Source is focused" 478 ); 479 480 // Note that the thread item also contains the extension name 481 const contentScriptGroupItem = findSourceTreeGroupByName( 482 dbg, 483 "Test content script extension" 484 ); 485 ok(contentScriptGroupItem, "Found the group item for the content script"); 486 ok( 487 contentScriptGroupItem.querySelector("span.dbg-img-extension"), 488 "The group has the extension icon" 489 ); 490 assertSourceIcon(dbg, "content_script.js", "javascript"); 491 492 for (let i = 1; i < 3; i++) { 493 info( 494 `Reloading tab (${i} time), the content script should always be reselected` 495 ); 496 gBrowser.reloadTab(gBrowser.selectedTab); 497 await waitForSelectedSource(dbg, "content_script.js"); 498 ok( 499 findElementWithSelector(dbg, ".sources-list .focused"), 500 "Source is focused" 501 ); 502 } 503 await dbg.toolbox.closeToolbox(); 504 505 await extension.unload(); 506 }); 507 508 add_task(async function testSourceTreeWithEncodedPaths() { 509 const httpServer = createTestHTTPServer(); 510 httpServer.registerContentType("html", "text/html"); 511 httpServer.registerContentType("js", "application/javascript"); 512 513 httpServer.registerPathHandler("/index.html", function (request, response) { 514 response.setStatusLine(request.httpVersion, 200, "OK"); 515 response.write(`<!DOCTYPE html> 516 <html> 517 <head> 518 <script src="/my folder/my file.js"></script> 519 <script src="/malformedUri.js?%"></script> 520 </head> 521 <body> 522 <h1>Encoded scripts paths</h1> 523 </body> 524 `); 525 }); 526 httpServer.registerPathHandler( 527 encodeURI("/my folder/my file.js"), 528 function (request, response) { 529 response.setStatusLine(request.httpVersion, 200, "OK"); 530 response.setHeader("Content-Type", "application/javascript", false); 531 response.write(`const x = 42`); 532 } 533 ); 534 httpServer.registerPathHandler( 535 "/malformedUri.js", 536 function (request, response) { 537 response.setStatusLine(request.httpVersion, 200, "OK"); 538 response.setHeader("Content-Type", "application/javascript", false); 539 response.write(`const y = "malformed"`); 540 } 541 ); 542 const port = httpServer.identity.primaryPort; 543 544 const dbg = await initDebuggerWithAbsoluteURL( 545 `http://localhost:${port}/index.html`, 546 "my file.js" 547 ); 548 549 await waitForSourcesInSourceTree(dbg, ["my file.js", "malformedUri.js?%"]); 550 ok( 551 true, 552 "source name are decoded in the tree, and malformed uri source are displayed" 553 ); 554 is( 555 // We don't have any specific class on the folder item, so let's target the folder 556 // icon next sibling, which is the directory label. 557 findElementWithSelector( 558 dbg, 559 ".sources-panel .node .dbg-img-folder + .label" 560 ).innerText, 561 "my folder", 562 "folder name is decoded in the tree" 563 ); 564 }); 565 566 /** 567 * Assert the location displayed in the breakpoint list, in the right sidebar. 568 * 569 * @param {object} dbg 570 * @param {string} label 571 * The expected displayed location 572 * @param {number} index 573 * The position of the breakpoint in the list to verify 574 */ 575 function assertBreakpointHeading(dbg, label, index) { 576 const breakpointHeading = findAllElements(dbg, "breakpointHeadings")[index] 577 .innerText; 578 is(breakpointHeading, label, `Breakpoint heading is ${label}`); 579 }