browser_dbg-features-source-text-content.js (18682B)
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 focus on asserting the source content displayed in CodeMirror 7 * when we open a source from the SourceTree (or by any other means). 8 * 9 * The source content is being fetched from the server only on-demand. 10 * The main shortcoming is about sources being GC-ed. This only happens 11 * when we open the debugger on an already loaded page. 12 * When we (re)load a page while the debugger is opened, sources are never GC-ed. 13 * There are also specifics related to HTML page having inline scripts. 14 * Also, as this data is fetched on-demand, there is a loading prompt 15 * being displayed while the source is being fetched from the server. 16 */ 17 18 "use strict"; 19 20 const httpServer = createTestHTTPServer(); 21 const BASE_URL = `http://localhost:${httpServer.identity.primaryPort}/`; 22 const loadCounts = {}; 23 24 /** 25 * Simple tests, asserting that we correctly display source text content in CodeMirror 26 */ 27 const NAMED_EVAL_CONTENT = `function namedEval() {}; console.log('eval script'); //# sourceURL=named-eval.js`; 28 const NEW_FUNCTION_CONTENT = 29 "console.log('new function'); //# sourceURL=new-function.js"; 30 const INDEX_PAGE_CONTENT = `<!DOCTYPE html> 31 <html> 32 <head> 33 <script type="text/javascript" src="/normal-script.js"></script> 34 <script type="text/javascript" src="/slow-loading-script.js"></script> 35 <script type="text/javascript" src="/http-error-script.js"></script> 36 <script type="text/javascript" src="/same-url.js"></script> 37 <script> 38 console.log("inline script"); 39 </script> 40 <script> 41 console.log("second inline script"); 42 this.evaled1 = eval("${NAMED_EVAL_CONTENT}"); 43 44 // Load same-url.js in various different ways 45 this.evaled2 = eval("function sameUrlEval() {}; //# sourceURL=same-url.js"); 46 47 const script = document.createElement("script"); 48 script.src = "same-url.js"; 49 document.documentElement.append(script); 50 51 // Ensure loading the same-url.js file *after* 52 // the iframe is done loading. So that we have a deterministic load order. 53 window.onload = () => { 54 this.worker = new Worker("same-url.js"); 55 this.worker.postMessage("foo"); 56 }; 57 58 this.newFunction = new Function("${NEW_FUNCTION_CONTENT}"); 59 60 // This method will be called via invokeInTab. 61 let inFunctionIncrement = 1; 62 function breakInNewFunction() { 63 new Function("a\\n", "b" +inFunctionIncrement++, "debugger;")(); 64 } 65 </script> 66 </head> 67 <body> 68 <iframe src="iframe.html"></iframe> 69 </body> 70 </html>`; 71 const IFRAME_CONTENT = `<!DOCTYPE html> 72 <html> 73 <head> 74 <script type="text/javascript" src="/same-url.js"></script> 75 </head> 76 <body> 77 <script> 78 console.log("First inline script"); 79 </script> 80 <script> 81 console.log("Second inline script"); 82 </script> 83 </body> 84 </html>`; 85 86 httpServer.registerPathHandler("/index.html", (request, response) => { 87 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 88 response.setStatusLine(request.httpVersion, 200, "OK"); 89 response.write(INDEX_PAGE_CONTENT); 90 }); 91 92 httpServer.registerPathHandler("/normal-script.js", (request, response) => { 93 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 94 response.setHeader("Content-Type", "application/javascript"); 95 response.write(`console.log("normal script")`); 96 }); 97 httpServer.registerPathHandler( 98 "/slow-loading-script.js", 99 (request, response) => { 100 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 101 response.processAsync(); 102 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 103 setTimeout(function () { 104 response.setHeader("Content-Type", "application/javascript"); 105 response.write(`console.log("slow loading script")`); 106 response.finish(); 107 }, 1000); 108 } 109 ); 110 httpServer.registerPathHandler("/http-error-script.js", (request, response) => { 111 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 112 response.setStatusLine(request.httpVersion, 404, "Not found"); 113 response.write(`console.log("http error")`); 114 }); 115 httpServer.registerPathHandler("/same-url.js", (request, response) => { 116 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 117 const sameUrlLoadCount = loadCounts[request.path]; 118 // Prevents gecko from cache this request in order to force fetching 119 // a new, distinct content for each usage of this URL 120 response.setHeader("Cache-Control", "no-store"); 121 response.setHeader("Content-Type", "application/javascript"); 122 response.write(`console.log("same url #${sameUrlLoadCount}")`); 123 }); 124 httpServer.registerPathHandler("/iframe.html", (request, response) => { 125 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 126 response.setHeader("Content-Type", "text/html"); 127 response.write(IFRAME_CONTENT); 128 }); 129 add_task(async function testSourceTextContent() { 130 const dbg = await initDebuggerWithAbsoluteURL("about:blank"); 131 132 const waitForSources = [ 133 "index.html", 134 "normal-script.js", 135 "slow-loading-script.js", 136 "same-url.js", 137 "new-function.js", 138 "iframe.html", 139 ]; 140 141 // Load the document *once* the debugger is opened 142 // in order to avoid having any source being GC-ed. 143 await navigateToAbsoluteURL(dbg, BASE_URL + "index.html", ...waitForSources); 144 145 await selectSourceFromSourceTreeWithIndex( 146 dbg, 147 "new-function.js", 148 5, 149 "Select `new-function.js`" 150 ); 151 is( 152 getEditorContent(dbg), 153 `function anonymous(\n) {\n${NEW_FUNCTION_CONTENT}\n}` 154 ); 155 156 await selectSourceFromSourceTreeWithIndex( 157 dbg, 158 "normal-script.js", 159 6, 160 "Select `normal-script.js`" 161 ); 162 is(getEditorContent(dbg), `console.log("normal script")`); 163 164 await selectSourceFromSourceTreeWithIndex( 165 dbg, 166 "slow-loading-script.js", 167 8, 168 "Select `slow-loading-script.js`" 169 ); 170 is(getEditorContent(dbg), `console.log("slow loading script")`); 171 172 await selectSourceFromSourceTreeWithIndex( 173 dbg, 174 "index.html", 175 3, 176 "Select `index.html`" 177 ); 178 is(getEditorContent(dbg), INDEX_PAGE_CONTENT); 179 180 await selectSourceFromSourceTreeWithIndex( 181 dbg, 182 "named-eval.js", 183 4, 184 "Select `named-eval.js`" 185 ); 186 is(getEditorContent(dbg), NAMED_EVAL_CONTENT); 187 188 await selectSourceFromSourceTreeWithIndex( 189 dbg, 190 "same-url.js", 191 7, 192 "Select `same-url.js` in the Main Thread" 193 ); 194 195 is( 196 getEditorContent(dbg), 197 `console.log("same url #1")`, 198 "We get an arbitrary content for same-url, the first loaded one" 199 ); 200 201 const sameUrlSource = findSource(dbg, "same-url.js"); 202 const sourceActors = dbg.selectors.getSourceActorsForSource(sameUrlSource.id); 203 204 const mainThread = dbg.selectors 205 .getAllThreads() 206 .find(thread => thread.name == "Main Thread"); 207 208 is( 209 sourceActors.filter(actor => actor.thread == mainThread.actor).length, 210 3, 211 "same-url.js is loaded 3 times in the main thread" 212 ); 213 214 info(`Close the same-url.js from Main Thread`); 215 await closeTab(dbg, "same-url.js"); 216 217 info("Click on the iframe tree node to show sources in the iframe"); 218 await clickElement(dbg, "sourceDirectoryLabel", 9); 219 await waitForSourcesInSourceTree( 220 dbg, 221 [ 222 "index.html", 223 "named-eval.js", 224 "normal-script.js", 225 "slow-loading-script.js", 226 "same-url.js", 227 "iframe.html", 228 "same-url.js", 229 "new-function.js", 230 ], 231 { 232 noExpand: true, 233 } 234 ); 235 236 await selectSourceFromSourceTreeWithIndex( 237 dbg, 238 "same-url.js", 239 12, 240 "Select `same-url.js` in the iframe" 241 ); 242 243 is( 244 getEditorContent(dbg), 245 `console.log("same url #3")`, 246 "We get the expected content for same-url.js in the iframe" 247 ); 248 249 const iframeThread = dbg.selectors 250 .getAllThreads() 251 .find(thread => thread.name == `${BASE_URL}iframe.html`); 252 is( 253 sourceActors.filter(actor => actor.thread == iframeThread.actor).length, 254 1, 255 "same-url.js is loaded one time in the iframe thread" 256 ); 257 258 info(`Close the same-url.js from the iframe`); 259 await closeTab(dbg, "same-url.js"); 260 261 info("Click on the worker tree node to show sources in the worker"); 262 263 await clickElement(dbg, "sourceDirectoryLabel", 13); 264 265 const workerSources = [ 266 "index.html", 267 "named-eval.js", 268 "normal-script.js", 269 "slow-loading-script.js", 270 "same-url.js", 271 "iframe.html", 272 "same-url.js", 273 "new-function.js", 274 "same-url.js", 275 ]; 276 277 await waitForSourcesInSourceTree(dbg, workerSources, { 278 noExpand: true, 279 }); 280 281 await selectSourceFromSourceTreeWithIndex( 282 dbg, 283 "same-url.js", 284 15, 285 "Select `same-url.js` in the worker" 286 ); 287 288 is( 289 getEditorContent(dbg), 290 `console.log("same url #4")`, 291 "We get the expected content for same-url.js worker" 292 ); 293 294 const workerThread = dbg.selectors 295 .getAllThreads() 296 .find(thread => thread.url == `${BASE_URL}same-url.js`); 297 298 is( 299 sourceActors.filter(actor => actor.thread == workerThread.actor).length, 300 1, 301 "same-url.js is loaded one time in the worker thread" 302 ); 303 304 await selectSource(dbg, "iframe.html"); 305 is(getEditorContent(dbg), IFRAME_CONTENT); 306 307 ok( 308 !sourceExists(dbg, "http-error-script.js"), 309 "scripts with HTTP error code do not appear in the source list" 310 ); 311 312 info( 313 "Verify that breaking in a source without url displays the right content" 314 ); 315 let onNewSource = waitForDispatch(dbg.store, "ADD_SOURCES"); 316 invokeInTab("breakInNewFunction"); 317 await waitForPaused(dbg); 318 let { sources } = await onNewSource; 319 is(sources.length, 1, "Got a unique source related to new Function source"); 320 let newFunctionSource = sources[0]; 321 // We acknowledge the function header as well as the new line in the first argument 322 await assertPausedAtSourceAndLine(dbg, newFunctionSource.id, 4, 0); 323 is(getEditorContent(dbg), "function anonymous(a\n,b1\n) {\ndebugger;\n}"); 324 await resume(dbg); 325 326 info( 327 "Break a second time in a source without url to verify we display the right content" 328 ); 329 onNewSource = waitForDispatch(dbg.store, "ADD_SOURCES"); 330 invokeInTab("breakInNewFunction"); 331 await waitForPaused(dbg); 332 ({ sources } = await onNewSource); 333 is(sources.length, 1, "Got a unique source related to new Function source"); 334 newFunctionSource = sources[0]; 335 // We acknowledge the function header as well as the new line in the first argument 336 await assertPausedAtSourceAndLine(dbg, newFunctionSource.id, 4, 0); 337 is(getEditorContent(dbg), "function anonymous(a\n,b2\n) {\ndebugger;\n}"); 338 await resume(dbg); 339 340 // As we are loading the page while the debugger is already opened, 341 // none of the resources are loaded twice. 342 is(loadCounts["/index.html"], 1, "We loaded index.html only once"); 343 is( 344 loadCounts["/normal-script.js"], 345 1, 346 "We loaded normal-script.js only once" 347 ); 348 is( 349 loadCounts["/slow-loading-script.js"], 350 1, 351 "We loaded slow-loading-script.js only once" 352 ); 353 is( 354 loadCounts["/same-url.js"], 355 4, 356 "We loaded same-url.js in 4 distinct ways (the named eval doesn't count)" 357 ); 358 // For some reason external to the debugger, we issue two requests to scripts having http error codes. 359 // These two requests are done before opening the debugger. 360 is( 361 loadCounts["/http-error-script.js"], 362 2, 363 "We loaded http-error-script.js twice, only before the debugger is opened" 364 ); 365 366 await reload(dbg, "index.html", ...waitForSources); 367 368 // Verify that iframe source content is fetch correctly after reload 369 await selectSource(dbg, "iframe.html"); 370 is(getEditorContent(dbg), IFRAME_CONTENT); 371 }); 372 373 /** 374 * In this test, we force a GC before loading DevTools. 375 * So that Spidermonkey will no longer have access to the sources 376 * and another request should be issues to load the source text content. 377 */ 378 const GARBAGED_PAGE_CONTENT = `<!DOCTYPE html> 379 <html> 380 <head> 381 <script type="text/javascript" src="/garbaged-script.js"></script> 382 <script> 383 console.log("garbaged inline script"); 384 </script> 385 </head> 386 </html>`; 387 388 httpServer.registerPathHandler( 389 "/garbaged-collected.html", 390 (request, response) => { 391 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 392 response.setStatusLine(request.httpVersion, 200, "OK"); 393 response.write(GARBAGED_PAGE_CONTENT); 394 } 395 ); 396 397 httpServer.registerPathHandler("/garbaged-script.js", (request, response) => { 398 loadCounts[request.path] = (loadCounts[request.path] || 0) + 1; 399 response.setHeader("Content-Type", "application/javascript"); 400 response.write(`console.log("garbaged script ${loadCounts[request.path]}")`); 401 }); 402 add_task(async function testGarbageCollectedSourceTextContent() { 403 const tab = await addTab(BASE_URL + "garbaged-collected.html"); 404 is( 405 loadCounts["/garbaged-collected.html"], 406 1, 407 "The HTML page is loaded once before opening the DevTools" 408 ); 409 is( 410 loadCounts["/garbaged-script.js"], 411 1, 412 "The script is loaded once before opening the DevTools" 413 ); 414 415 // Force freeing both the HTML page and script in memory 416 // so that the debugger has to fetch source content from http cache. 417 await SpecialPowers.spawn(tab.linkedBrowser, [], () => { 418 Cu.forceGC(); 419 }); 420 421 const toolbox = await openToolboxForTab(tab, "jsdebugger"); 422 const dbg = createDebuggerContext(toolbox); 423 await waitForSources(dbg, "garbaged-collected.html", "garbaged-script.js"); 424 425 await selectSource(dbg, "garbaged-script.js"); 426 // XXX Bug 1758454 - Source content of GC-ed script can be wrong! 427 // Even if we have to issue a new HTTP request for this source, 428 // we should be using HTTP cache and retrieve the first served version which 429 // is the one that actually runs in the page! 430 // We should be displaying `console.log("garbaged script 1")`, 431 // but instead, a new HTTP request is dispatched and we get a new content. 432 is(getEditorContent(dbg), `console.log("garbaged script 2")`); 433 434 await selectSource(dbg, "garbaged-collected.html"); 435 is(getEditorContent(dbg), GARBAGED_PAGE_CONTENT); 436 437 is( 438 loadCounts["/garbaged-collected.html"], 439 2, 440 "We loaded the html page once as we haven't tried to display it in the debugger (2)" 441 ); 442 is( 443 loadCounts["/garbaged-script.js"], 444 2, 445 "We loaded the garbaged script twice as we lost its content" 446 ); 447 }); 448 449 /** 450 * Test failures when trying to open the source text content. 451 * 452 * In this test we load an html page 453 * - with inline source (so that it shows up in the debugger) 454 * - it first loads fine so that it shows up 455 * - initDebuggerWithAbsoluteURL will first load the document before the debugger 456 * - so the debugger will have to fetch the html page content via a network request 457 * - the test page will return a connection reset error on the second load attempt 458 */ 459 let loadCount = 0; 460 httpServer.registerPathHandler( 461 "/200-then-connection-reset.html", 462 (request, response) => { 463 loadCount++; 464 if (loadCount > 1) { 465 response.seizePower(); 466 response.bodyOutPutStream.close(); 467 response.finish(); 468 return; 469 } 470 response.setStatusLine(request.httpVersion, 200, "OK"); 471 response.write(`<!DOCTYPE html><script>console.log("200 page");</script>`); 472 } 473 ); 474 add_task(async function testFailingHtmlSource() { 475 info("Test failure in retrieving html page sources"); 476 477 // initDebuggerWithAbsoluteURL will first load the document once before the debugger, 478 // then the debugger will have to fetch the html page content via a network request 479 // therefore the test page will encounter a connection reset error on the second load attempt 480 const dbg = await initDebuggerWithAbsoluteURL( 481 BASE_URL + "200-then-connection-reset.html", 482 "200-then-connection-reset.html" 483 ); 484 485 // We can't select the HTML page as its source content isn't fetched 486 // (waitForSelectedSource doesn't resolve) 487 // Note that it is important to load the page *before* opening the page 488 // so that the thread actor has to request the page content and will fail 489 const source = findSource(dbg, "200-then-connection-reset.html"); 490 await dbg.actions.selectLocation(createLocation({ source }), { 491 keepContext: false, 492 }); 493 494 ok( 495 getEditorContent(dbg).includes("Could not load the source"), 496 "Display failure error" 497 ); 498 }); 499 500 /** 501 * In this test we try to reproduce the "Loading..." message. 502 * This may happen when opening an HTML source that was loaded *before* 503 * opening DevTools. The thread actor will have to issue a new HTTP request 504 * to load the source content. 505 */ 506 let loadCount2 = 0; 507 let slowLoadingPageResolution = null; 508 httpServer.registerPathHandler( 509 "/slow-loading-page.html", 510 (request, response) => { 511 loadCount2++; 512 if (loadCount2 > 1) { 513 response.processAsync(); 514 slowLoadingPageResolution = function () { 515 response.write( 516 `<!DOCTYPE html><script>console.log("slow-loading-page:second-load");</script>` 517 ); 518 response.finish(); 519 }; 520 return; 521 } 522 response.write( 523 `<!DOCTYPE html><script>console.log("slow-loading-page:first-load");</script>` 524 ); 525 } 526 ); 527 add_task(async function testLoadingHtmlSource() { 528 info("Test loading progress of html page sources"); 529 const dbg = await initDebuggerWithAbsoluteURL( 530 BASE_URL + "slow-loading-page.html", 531 "slow-loading-page.html" 532 ); 533 534 const onSelected = selectSource(dbg, "slow-loading-page.html"); 535 await waitFor( 536 () => getEditorContent(dbg) == DEBUGGER_L10N.getStr("loadingText"), 537 "Wait for the source to be displayed as loading" 538 ); 539 540 info("Wait for a second HTTP request to be made for the html page"); 541 await waitFor( 542 () => slowLoadingPageResolution, 543 "Wait for the html page to be queried a second time" 544 ); 545 is( 546 getEditorContent(dbg), 547 DEBUGGER_L10N.getStr("loadingText"), 548 "The source is still loading until we release the network request" 549 ); 550 551 slowLoadingPageResolution(); 552 info("Wait for the source to be fully selected and loaded"); 553 await onSelected; 554 555 // Note that, even if the thread actor triggers a new HTTP request, 556 // it will use the HTTP cache and retrieve the first request content. 557 // This is actually relevant as that's the source that actually runs in the page! 558 // 559 // XXX Bug 1758458 - the source content is wrong. 560 // We should be seeing the whole HTML page content, 561 // whereas we only see the inline source text content. 562 is(getEditorContent(dbg), `console.log("slow-loading-page:first-load");`); 563 });