browser_net_search-results.js (19688B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 /** 7 * Test search match functionality. 8 * Search panel is visible and clicking matches shows them in the request details. 9 */ 10 11 const { PluralForm } = require("resource://devtools/shared/plural-form.js"); 12 13 add_task(async function () { 14 const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { 15 requestCount: 1, 16 }); 17 info("Starting test... "); 18 19 const { document, store, windowRequire } = monitor.panelWin; 20 21 // Action should be processed synchronously in tests. 22 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 23 store.dispatch(Actions.batchEnable(false)); 24 25 const SEARCH_STRING = "test"; 26 // Execute two XHRs and wait until they are finished. 27 const URLS = [ 28 HTTPS_SEARCH_SJS + "?value=test1", 29 HTTPS_SEARCH_SJS + "?value=test2", 30 ]; 31 32 const wait = waitForNetworkEvents(monitor, 2); 33 await SpecialPowers.spawn(tab.linkedBrowser, [URLS], makeRequests); 34 await wait; 35 36 // Open the Search panel 37 await store.dispatch(Actions.openSearch()); 38 39 // Fill Filter input with text and check displayed messages. 40 // The filter should be focused automatically. 41 typeInNetmonitor(SEARCH_STRING, monitor); 42 EventUtils.synthesizeKey("KEY_Enter"); 43 44 // Wait until there are two resources rendered in the results 45 await waitForDOMIfNeeded( 46 document, 47 ".search-panel-content .treeRow.resourceRow", 48 2 49 ); 50 51 const searchMatchContents = document.querySelectorAll( 52 ".search-panel-content .treeRow .treeIcon" 53 ); 54 55 for (let i = searchMatchContents.length - 1; i >= 0; i--) { 56 clickElement(searchMatchContents[i], monitor); 57 } 58 59 // Wait until there are two resources rendered in the results 60 await waitForDOMIfNeeded( 61 document, 62 ".search-panel-content .treeRow.resultRow", 63 12 64 ); 65 66 // Check the matches 67 const matches = document.querySelectorAll( 68 ".search-panel-content .treeRow.resultRow" 69 ); 70 71 await checkSearchResult( 72 monitor, 73 matches[0], 74 "#headers-panel", 75 ".url-preview .properties-view", 76 ".treeRow", 77 [SEARCH_STRING] 78 ); 79 await checkSearchResult( 80 monitor, 81 matches[1], 82 "#headers-panel", 83 "#responseHeaders .properties-view", 84 ".treeRow.selected", 85 [SEARCH_STRING] 86 ); 87 await checkSearchResult( 88 monitor, 89 matches[2], 90 "#headers-panel", 91 "#requestHeaders .properties-view", 92 ".treeRow.selected", 93 [SEARCH_STRING] 94 ); 95 await checkSearchResult( 96 monitor, 97 matches[3], 98 "#cookies-panel", 99 "#responseCookies .properties-view", 100 ".treeRow.selected", 101 [SEARCH_STRING] 102 ); 103 await checkSearchResult( 104 monitor, 105 matches[4], 106 "#response-panel", 107 ".cm-content", 108 ".cm-line", 109 [SEARCH_STRING] 110 ); 111 await checkSearchResult( 112 monitor, 113 matches[5], 114 "#headers-panel", 115 ".url-preview .properties-view", 116 ".treeRow", 117 [SEARCH_STRING] 118 ); 119 await checkSearchResult( 120 monitor, 121 matches[6], 122 "#headers-panel", 123 "#responseHeaders .properties-view", 124 ".treeRow.selected", 125 [SEARCH_STRING] 126 ); 127 await checkSearchResult( 128 monitor, 129 matches[7], 130 "#headers-panel", 131 "#requestHeaders .properties-view", 132 ".treeRow.selected", 133 [SEARCH_STRING] 134 ); 135 await checkSearchResult( 136 monitor, 137 matches[8], 138 "#headers-panel", 139 "#requestHeaders .properties-view", 140 ".treeRow.selected", 141 [SEARCH_STRING] 142 ); 143 await checkSearchResult( 144 monitor, 145 matches[9], 146 "#cookies-panel", 147 "#responseCookies .properties-view", 148 ".treeRow.selected", 149 [SEARCH_STRING] 150 ); 151 await checkSearchResult( 152 monitor, 153 matches[10], 154 "#cookies-panel", 155 "#requestCookies .properties-view", 156 ".treeRow.selected", 157 [SEARCH_STRING] 158 ); 159 await checkSearchResult( 160 monitor, 161 matches[11], 162 "#response-panel", 163 ".cm-content", 164 ".cm-line", 165 [SEARCH_STRING] 166 ); 167 168 await teardown(monitor); 169 }); 170 171 /** 172 * Test the context menu feature inside the search match functionality. 173 * Search panel is visible and right-clicking matches shows the appropriate context-menu's. 174 */ 175 176 add_task(async function () { 177 const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { 178 requestCount: 1, 179 }); 180 info("Starting test... "); 181 182 const { document, store, windowRequire } = monitor.panelWin; 183 184 // Action should be processed synchronously in tests. 185 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 186 store.dispatch(Actions.batchEnable(false)); 187 188 const SEARCH_STRING = "matchingResult"; 189 const matchingUrls = [ 190 HTTPS_SEARCH_SJS + "?value=matchingResult1", 191 HTTPS_SEARCH_SJS + "?value=matchingResult2", 192 ]; 193 const nonMatchingUrls = [HTTPS_SEARCH_SJS + "?value=somethingDifferent"]; 194 195 const wait = waitForNetworkEvents( 196 monitor, 197 matchingUrls.length + nonMatchingUrls.length 198 ); 199 await SpecialPowers.spawn(tab.linkedBrowser, [matchingUrls], makeRequests); 200 await SpecialPowers.spawn(tab.linkedBrowser, [nonMatchingUrls], makeRequests); 201 await wait; 202 203 // Open the Search panel 204 await store.dispatch(Actions.openSearch()); 205 206 // Fill Filter input with text and check displayed messages. 207 // The filter should be focused automatically. 208 typeInNetmonitor(SEARCH_STRING, monitor); 209 EventUtils.synthesizeKey("KEY_Enter"); 210 211 // Wait for all the updates to complete 212 await waitForAllNetworkUpdateEvents(); 213 214 // Wait until there are two resources rendered in the results 215 await waitForDOMIfNeeded( 216 document, 217 ".search-panel-content .treeRow.resourceRow", 218 2 219 ); 220 221 const resourceMatches = document.querySelectorAll( 222 ".search-panel-content .treeRow .treeIcon" 223 ); 224 225 // open content matches for first resource: 226 const firstResourceMatch = resourceMatches[0]; 227 clickElement(firstResourceMatch, monitor); 228 229 // Wait until the expanded resource is rendered in the results 230 await waitForDOMIfNeeded( 231 document, 232 ".search-panel-content .treeRow.resultRow", 233 1 234 ); 235 236 // Check the content matches 237 const contentMatches = document.querySelectorAll( 238 ".search-panel-content .treeRow.resultRow" 239 ); 240 241 // test context menu entries for contained content: 242 const firstContentMatch = contentMatches[0]; 243 ok( 244 document.querySelector(".treeRow.selected.opened"), 245 "The previous line, which is the selected line, is expanded to show the result row" 246 ); 247 await checkContentMenuCopy(firstContentMatch, matchingUrls[0], monitor); 248 249 // test the context menu entries for resources 250 const secondResourceMatch = resourceMatches[1]; 251 await checkResourceMenuCopyUrl(secondResourceMatch, matchingUrls[1], monitor); 252 253 // checkResourceMenuResend will trigger a new request, which will be added to the search results 254 await checkResourceMenuResend(secondResourceMatch, monitor); 255 256 // Assert that the previously expanded search result is kept expanded when a new request is added 257 ok( 258 document.querySelector(".treeRow.selected.opened"), 259 "The previous line is still expanded after having added a new request is the result list" 260 ); 261 262 await checkResourceMenuBlockUnblock( 263 secondResourceMatch, 264 matchingUrls[1], 265 monitor 266 ); 267 await checkSaveAllAsHARWithContextMenu( 268 secondResourceMatch, 269 matchingUrls, 270 monitor 271 ); 272 273 // reload tab 274 const waitForEvents = waitForNetworkEvents(monitor, 1); 275 tab.linkedBrowser.reload(); 276 await waitForEvents; 277 278 // test that the context menu entries are not available anymore: 279 await checkResourceMenuNotAvailbale(secondResourceMatch, monitor); 280 281 await teardown(monitor); 282 }); 283 284 add_task(async function searchWithRequestOnUnload() { 285 const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { 286 requestCount: 1, 287 }); 288 info("Starting test... "); 289 290 const { document, store, windowRequire } = monitor.panelWin; 291 292 // Action should be processed synchronously in tests. 293 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 294 store.dispatch(Actions.batchEnable(false)); 295 296 await SpecialPowers.spawn( 297 tab.linkedBrowser, 298 [HTTPS_SEARCH_SJS + "?value=test1"], 299 function (url) { 300 content.addEventListener("unload", () => { 301 content.wrappedJSObject.get(url); 302 }); 303 } 304 ); 305 306 const SEARCH_STRING = "html_custom-get-page.html"; 307 308 // reload tab, expect the html page and the unload request 309 const waitForEvents = waitForNetworkEvents(monitor, 2); 310 tab.linkedBrowser.reload(); 311 await waitForEvents; 312 313 // Open the Search panel 314 await store.dispatch(Actions.openSearch()); 315 316 // Fill Filter input with text and check displayed messages. 317 // The filter should be focused automatically. 318 typeInNetmonitor(SEARCH_STRING, monitor); 319 EventUtils.synthesizeKey("KEY_Enter"); 320 321 // Wait until there are two resources rendered in the results 322 await waitForDOMIfNeeded( 323 document, 324 ".search-panel-content .treeRow.resourceRow", 325 1 326 ); 327 328 // Wait until there are two resources rendered in the results 329 await waitForDOMIfNeeded(document, ".search-panel .status-bar-label"); 330 const statusBar = document.querySelector(".search-panel .status-bar-label"); 331 const matchingLines = PluralForm.get( 332 1, 333 L10N.getStr("netmonitor.search.status.labels.matchingLines") 334 ).replace("#1", 1); 335 const matchingFiles = PluralForm.get( 336 1, 337 L10N.getStr("netmonitor.search.status.labels.fileCount") 338 ).replace("#1", 1); 339 is( 340 statusBar.textContent, 341 L10N.getFormatStr( 342 "netmonitor.search.status.labels.done", 343 matchingLines, 344 matchingFiles 345 ), 346 "Search completed" 347 ); 348 349 await teardown(monitor); 350 }); 351 352 // Asserts that the content is scrolled to show the correct matched content 353 // on the line when a match is selected from the network search list. 354 add_task(async function testContentIsScrolledWhenMatchIsSelected() { 355 const httpServer = createTestHTTPServer(); 356 httpServer.registerPathHandler(`/`, function (request, response) { 357 response.setStatusLine(request.httpVersion, 200, "OK"); 358 response.write(`<html><meta charset=utf8> 359 <script type="text/javascript" src="/script.js"></script> 360 <h1>Test matches in scrolled content</h1> 361 </html>`); 362 }); 363 364 // The "data" path takes a size query parameter and will return a body of the 365 // requested size. 366 httpServer.registerPathHandler("/script.js", function (request, response) { 367 response.setHeader("Content-Type", "text/javascript"); 368 response.setStatusLine(request.httpVersion, 200, "OK"); 369 response.write( 370 `${Array.from({ length: 40 }, (_, i) => `// line ${++i}x`).join("\n")}` 371 ); 372 }); 373 374 const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`; 375 376 const { tab, monitor } = await initNetMonitor(TEST_URI, { 377 requestCount: 1, 378 }); 379 info("Starting test... "); 380 const { document, store, windowRequire } = monitor.panelWin; 381 382 // Action should be processed synchronously in tests. 383 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 384 store.dispatch(Actions.batchEnable(false)); 385 386 // reload tab, expect the html page and the script request 387 const waitForEvents = waitForNetworkEvents(monitor, 2); 388 tab.linkedBrowser.reload(); 389 await waitForEvents; 390 391 // Open the Search panel 392 await store.dispatch(Actions.openSearch()); 393 394 // Fill search input with text and check displayed messages. 395 const waitForResult = waitFor(() => 396 document.querySelector(".search-panel-content .treeRow.resourceRow") 397 ); 398 typeInNetmonitor("line 3x", monitor); 399 EventUtils.synthesizeKey("KEY_Enter"); 400 await waitForResult; 401 402 const result = document.querySelector( 403 ".search-panel-content .treeRow.resourceRow" 404 ); 405 // Click the matches to 406 const waitForMatches = waitFor( 407 () => 408 document.querySelectorAll(".search-panel-content .treeRow.resultRow") 409 ?.length == 1 410 ); 411 clickElement(result, monitor); 412 await waitForMatches; 413 414 const matches = document.querySelectorAll( 415 ".search-panel-content .treeRow.resultRow" 416 ); 417 // Click the first result match 418 const waitForResponsePanelContent = waitFor(() => 419 document.querySelector(`#response-panel .cm-content`) 420 ); 421 clickElement(matches[0], monitor); 422 await waitForResponsePanelContent; 423 424 const editor = getCMEditor(monitor); 425 is( 426 editor.getSelectionCursor().from.line, 427 3, 428 "The content on line 3 is highlighted" 429 ); 430 is( 431 editor.getText(editor.getSelectionCursor().from.line), 432 "// line 3x", 433 "The content on the line with cursor (line 3) is correct" 434 ); 435 436 // Set the cursor to the bottom of the document 437 let onScrolled = waitForEditorScrolling(monitor); 438 await editor.setCursorAt(40, 0); 439 await onScrolled; 440 441 is( 442 editor.getSelectionCursor().from.line, 443 40, 444 "The content on line 40 is highlighted" 445 ); 446 is( 447 editor.getText(editor.getSelectionCursor().from.line), 448 "// line 40x", 449 "The content on the line with cursor (line 40) is correct" 450 ); 451 452 // Click the first result match again 453 onScrolled = waitForEditorScrolling(monitor); 454 clickElement(matches[0], monitor); 455 await onScrolled; 456 457 // The editor should scroll back up and the matched content on line 3 should be highlighted 458 459 is( 460 editor.getSelectionCursor().from.line, 461 3, 462 "The content on line 3 is highlighted" 463 ); 464 is( 465 editor.getText(editor.getSelectionCursor().from.line), 466 "// line 3x", 467 "The content on the line with cursor (line 3) is correct" 468 ); 469 }); 470 471 async function makeRequests(urls) { 472 await content.wrappedJSObject.get(urls[0]); 473 await content.wrappedJSObject.get(urls[1]); 474 info("XHR Requests executed"); 475 } 476 477 async function checkContentMenuCopy( 478 contentMatch, 479 expectedClipboardValue, 480 monitor 481 ) { 482 EventUtils.sendMouseEvent({ type: "contextmenu" }, contentMatch); 483 484 // execute the copy command: 485 await waitForClipboardPromise(async function setup() { 486 await selectContextMenuItem( 487 monitor, 488 "properties-view-context-menu-copyvalue" 489 ); 490 }, expectedClipboardValue); 491 } 492 493 async function checkResourceMenuCopyUrl( 494 resourceMatch, 495 expectedClipboardValue, 496 monitor 497 ) { 498 // trigger context menu: 499 EventUtils.sendMouseEvent({ type: "contextmenu" }, resourceMatch); 500 501 // select the context menu entry 'copy-url': 502 await waitForClipboardPromise(async function setup() { 503 await selectContextMenuItem(monitor, "request-list-context-copy-url"); 504 }, expectedClipboardValue); 505 } 506 507 async function checkResourceMenuResend(resourceMatch, monitor) { 508 const { store, windowRequire } = monitor.panelWin; 509 510 const { getSelectedRequest, getDisplayedRequests } = windowRequire( 511 "devtools/client/netmonitor/src/selectors/index" 512 ); 513 514 // expect the appearing of a new request in the list: 515 const displayedRequests = getDisplayedRequests(store.getState()); 516 const originalResourceIds = displayedRequests.map(r => r.id); 517 const expectedNrOfRequestsAfterResend = displayedRequests.length + 1; 518 519 // define wait functionality, that waits until a new entry appears with different ID: 520 const waitForNewRequest = waitUntil(() => { 521 const newResourceId = getSelectedRequest(store.getState())?.id; 522 return ( 523 getDisplayedRequests(store.getState()).length == 524 expectedNrOfRequestsAfterResend && 525 !originalResourceIds.includes(newResourceId) 526 ); 527 }); 528 529 // click the context menu 'resend' 530 EventUtils.sendMouseEvent({ type: "contextmenu" }, resourceMatch); 531 await selectContextMenuItem(monitor, "request-list-context-resend-only"); 532 await waitForNewRequest; 533 } 534 535 async function checkResourceMenuBlockUnblock(resourceMatch, blockUrl, monitor) { 536 const { store, windowRequire } = monitor.panelWin; 537 538 // block resource: 539 await toggleBlockedUrl(resourceMatch, monitor, store); 540 541 // assert that there is now 1 blocked URL: 542 is( 543 store.getState().requestBlocking.blockedUrls.length, 544 1, 545 "There should be 1 blocked URL" 546 ); 547 is( 548 store.getState().requestBlocking.blockedUrls[0].url, 549 blockUrl, 550 `The blocked URL should be '${blockUrl}'` 551 ); 552 553 // Open the Search panel again 554 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 555 await store.dispatch(Actions.openSearch()); 556 557 // block resource: 558 await toggleBlockedUrl(resourceMatch, monitor, store, "unblock"); 559 560 // assert that there is no blocked URL anymore: 561 is( 562 store.getState().requestBlocking.blockedUrls.length, 563 0, 564 "There should be no blocked URL" 565 ); 566 } 567 568 async function checkSaveAllAsHARWithContextMenu( 569 resourceMatch, 570 expectedUrls, 571 monitor 572 ) { 573 const { HarMenuUtils } = monitor.panelWin.windowRequire( 574 "devtools/client/netmonitor/src/har/har-menu-utils" 575 ); 576 577 EventUtils.sendMouseEvent({ type: "mousedown" }, resourceMatch); 578 EventUtils.sendMouseEvent({ type: "contextmenu" }, resourceMatch); 579 580 info("Trigger Copy All As HAR from the context menu"); 581 const onHarCopyDone = HarMenuUtils.once("copy-all-as-har-done"); 582 await selectContextMenuItem(monitor, "request-list-context-copy-all-as-har"); 583 const jsonString = await onHarCopyDone; 584 info("exported JSON:\n" + jsonString); 585 const parsedJson = JSON.parse(jsonString); 586 587 is( 588 parsedJson?.log?.entries?.length, 589 expectedUrls.length, 590 "Expected length of " + expectedUrls.length 591 ); 592 for (let i = 0; i < expectedUrls.length; i++) { 593 is( 594 parsedJson.log.entries[i].request?.url, 595 expectedUrls[i], 596 "Expected url was '" + expectedUrls[i] + "'" 597 ); 598 } 599 } 600 601 async function checkResourceMenuNotAvailbale(resourceMatch, monitor) { 602 // trigger context menu: 603 EventUtils.sendMouseEvent({ type: "contextmenu" }, resourceMatch); 604 605 is( 606 !!getContextMenuItem( 607 monitor, 608 "simple-view-context-menu-request-not-available-anymore" 609 ), 610 true, 611 "context menu item 'not-available' should be present" 612 ); 613 is( 614 !!getContextMenuItem(monitor, "request-list-context-resend-only"), 615 false, 616 "context menu item 'resend' should not be present" 617 ); 618 is( 619 !!getContextMenuItem(monitor, "request-list-context-copy-all-as-har"), 620 false, 621 "context menu item 'copy all as HAR' should not be present" 622 ); 623 is( 624 !!getContextMenuItem(monitor, "netmonitor.context.blockURL"), 625 false, 626 "context menu item 'block URL' should not be present" 627 ); 628 } 629 630 /** 631 * Check whether the search result is correctly linked with the related information 632 */ 633 async function checkSearchResult( 634 monitor, 635 match, 636 panelSelector, 637 panelContentSelector, 638 panelDetailSelector, 639 expected 640 ) { 641 const { document } = monitor.panelWin; 642 643 // Scroll the match into view so that it's clickable 644 match.scrollIntoView(); 645 646 // Click on the match to show it 647 clickElement(match, monitor); 648 649 console.log(`${panelSelector} ${panelContentSelector}`); 650 await waitFor(() => 651 document.querySelector(`${panelSelector} ${panelContentSelector}`) 652 ); 653 654 const tabpanel = document.querySelector(panelSelector); 655 const content = tabpanel.querySelectorAll( 656 `${panelContentSelector} ${panelDetailSelector}` 657 ); 658 659 is( 660 content.length, 661 expected.length, 662 `There should be ${expected.length} item${ 663 expected.length === 1 ? "" : "s" 664 } displayed in this tabpanel` 665 ); 666 667 // Make sure only 1 item is selected 668 if (panelDetailSelector === ".treeRow.selected") { 669 const selectedElements = tabpanel.querySelectorAll(panelDetailSelector); 670 is( 671 selectedElements.length, 672 1, 673 `There should be only 1 item selected, found ${selectedElements.length} items selected` 674 ); 675 } 676 677 if (content.length === expected.length) { 678 for (let i = 0; i < expected.length; i++) { 679 is( 680 content[i].textContent.includes(expected[i]), 681 true, 682 `Content must include ${expected[i]}` 683 ); 684 } 685 } 686 }