head.js (56858B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 /* globals Task, openToolboxForTab, gBrowser */ 7 8 // shared-head.js handles imports, constants, and utility functions 9 // Load the shared-head file first. 10 Services.scriptloader.loadSubScript( 11 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 12 this 13 ); 14 15 // Import helpers for the new debugger 16 Services.scriptloader.loadSubScript( 17 "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", 18 this 19 ); 20 21 Services.scriptloader.loadSubScript( 22 "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js", 23 this 24 ); 25 26 var { 27 BrowserConsoleManager, 28 } = require("resource://devtools/client/webconsole/browser-console-manager.js"); 29 30 var WCUL10n = require("resource://devtools/client/webconsole/utils/l10n.js"); 31 const DOCS_GA_PARAMS = `?${new URLSearchParams({ 32 utm_source: "devtools", 33 utm_medium: "firefox-console-errors", 34 utm_campaign: "default", 35 })}`; 36 const GA_PARAMS = `?${new URLSearchParams({ 37 utm_source: "devtools", 38 utm_medium: "devtools-webconsole", 39 utm_campaign: "default", 40 })}`; 41 42 const wcActions = require("resource://devtools/client/webconsole/actions/index.js"); 43 44 registerCleanupFunction(async function () { 45 // Reset all cookies, tests loading sjs_slow-response-test-server.sjs will 46 // set a foo cookie which might have side effects on other tests. 47 Services.cookies.removeAll(); 48 }); 49 50 /** 51 * Add a new tab and open the toolbox in it, and select the webconsole. 52 * 53 * @param string url 54 * The URL for the tab to be opened. 55 * @param Boolean clearJstermHistory 56 * true (default) if the jsterm history should be cleared. 57 * @param String hostId (optional) 58 * The type of toolbox host to be used. 59 * @return Promise 60 * Resolves when the tab has been added, loaded and the toolbox has been opened. 61 * Resolves to the hud. 62 */ 63 async function openNewTabAndConsole(url, clearJstermHistory = true, hostId) { 64 const toolbox = await openNewTabAndToolbox(url, "webconsole", hostId); 65 const hud = toolbox.getCurrentPanel().hud; 66 67 if (clearJstermHistory) { 68 // Clearing history that might have been set in previous tests. 69 await hud.ui.wrapper.dispatchClearHistory(); 70 } 71 72 return hud; 73 } 74 75 /** 76 * Add a new tab with iframes, open the toolbox in it, and select the webconsole. 77 * 78 * @param string url 79 * The URL for the tab to be opened. 80 * @param Arra<string> iframes 81 * An array of URLs that will be added to the top document. 82 * @return Promise 83 * Resolves when the tab has been added, loaded, iframes loaded, and the toolbox 84 * has been opened. Resolves to the hud. 85 */ 86 async function openNewTabWithIframesAndConsole(tabUrl, iframes) { 87 // We need to add the tab and the iframes before opening the console in case we want 88 // to handle remote frames (we don't support creating frames target when the toolbox 89 // is already open). 90 await addTab(tabUrl); 91 await ContentTask.spawn( 92 gBrowser.selectedBrowser, 93 iframes, 94 async function (urls) { 95 const iframesLoadPromises = urls.map((url, i) => { 96 const iframe = content.document.createElement("iframe"); 97 iframe.classList.add(`iframe-${i + 1}`); 98 const onLoad = ContentTaskUtils.waitForEvent(iframe, "load"); 99 iframe.src = url; 100 content.document.body.append(iframe); 101 return onLoad; 102 }); 103 104 await Promise.all(iframesLoadPromises); 105 } 106 ); 107 108 return openConsole(); 109 } 110 111 /** 112 * Open a new window with a tab,open the toolbox, and select the webconsole. 113 * 114 * @param string url 115 * The URL for the tab to be opened. 116 * @return Promise<{win, hud, tab}> 117 * Resolves when the tab has been added, loaded and the toolbox has been opened. 118 * Resolves to the toolbox. 119 */ 120 async function openNewWindowAndConsole(url) { 121 const win = await BrowserTestUtils.openNewBrowserWindow(); 122 const tab = await addTab(url, { window: win }); 123 win.gBrowser.selectedTab = tab; 124 const hud = await openConsole(tab); 125 return { win, hud, tab }; 126 } 127 128 /** 129 * Subscribe to the store and log out stringinfied versions of messages. 130 * This is a helper function for debugging, to make is easier to see what 131 * happened during the test in the log. 132 * 133 * @param object hud 134 */ 135 function logAllStoreChanges(hud) { 136 const store = hud.ui.wrapper.getStore(); 137 // Adding logging each time the store is modified in order to check 138 // the store state in case of failure. 139 store.subscribe(() => { 140 const messages = [ 141 ...store.getState().messages.mutableMessagesById.values(), 142 ]; 143 const debugMessages = messages.map( 144 ({ id, type, parameters, messageText }) => { 145 return { id, type, parameters, messageText }; 146 } 147 ); 148 info( 149 "messages : " + 150 JSON.stringify(debugMessages, function (key, value) { 151 if (value && value.getGrip) { 152 return value.getGrip(); 153 } 154 return value; 155 }) 156 ); 157 }); 158 } 159 160 /** 161 * Wait for messages with given message type in the web console output, 162 * resolving once they are received. 163 * 164 * @param object options 165 * - hud: the webconsole 166 * - messages: Array[Object]. An array of messages to match. 167 * Current supported options: 168 * - text: {String} Partial text match in .message-body 169 * - typeSelector: {String} A part of selector for the message, to 170 * specify the message type. 171 * @return promise 172 * A promise that is resolved to an array of the message nodes 173 */ 174 function waitForMessagesByType({ hud, messages }) { 175 return new Promise(resolve => { 176 const matchedMessages = []; 177 hud.ui.on("new-messages", function messagesReceived(newMessages) { 178 for (const message of messages) { 179 if (message.matched) { 180 continue; 181 } 182 183 const typeSelector = message.typeSelector; 184 if (!typeSelector) { 185 throw new Error("typeSelector property is required"); 186 } 187 if (!typeSelector.startsWith(".")) { 188 throw new Error( 189 "typeSelector property start with a dot e.g. `.result`" 190 ); 191 } 192 const selector = ".message" + typeSelector; 193 194 for (const newMessage of newMessages) { 195 const messageBody = newMessage.node.querySelector(`.message-body`); 196 if ( 197 messageBody && 198 newMessage.node.matches(selector) && 199 messageBody.textContent.includes(message.text) 200 ) { 201 matchedMessages.push(newMessage); 202 message.matched = true; 203 const messagesLeft = messages.length - matchedMessages.length; 204 info( 205 `Matched a message with text: "${message.text}", ` + 206 (messagesLeft > 0 207 ? `still waiting for ${messagesLeft} messages.` 208 : `all messages received.`) 209 ); 210 break; 211 } 212 } 213 214 if (matchedMessages.length === messages.length) { 215 hud.ui.off("new-messages", messagesReceived); 216 resolve(matchedMessages); 217 return; 218 } 219 } 220 }); 221 }); 222 } 223 224 /** 225 * Wait for a message with the provided text and showing the provided repeat count. 226 * 227 * @param {object} hud : the webconsole 228 * @param {string} text : text included in .message-body 229 * @param {string} typeSelector : A part of selector for the message, to 230 * specify the message type. 231 * @param {number} repeat : expected repeat count in .message-repeats 232 */ 233 function waitForRepeatedMessageByType(hud, text, typeSelector, repeat) { 234 return waitFor(() => { 235 // Wait for a message matching the provided text. 236 const node = findMessageByType(hud, text, typeSelector); 237 if (!node) { 238 return false; 239 } 240 241 // Check if there is a repeat node with the expected count. 242 const repeatNode = node.querySelector(".message-repeats"); 243 if (repeatNode && parseInt(repeatNode.textContent, 10) === repeat) { 244 return node; 245 } 246 247 return false; 248 }); 249 } 250 251 /** 252 * Wait for a single message with given message type in the web console output, 253 * resolving with the first message that matches the query once it is received. 254 * 255 * @param {object} hud : the webconsole 256 * @param {string} text : text included in .message-body 257 * @param {string} typeSelector : A part of selector for the message, to 258 * specify the message type. 259 * @return promise 260 * A promise that is resolved to the message node 261 */ 262 async function waitForMessageByType(hud, text, typeSelector) { 263 const messages = await waitForMessagesByType({ 264 hud, 265 messages: [{ text, typeSelector }], 266 }); 267 return messages[0]; 268 } 269 270 /** 271 * Wait for the Source editor to be available. 272 * 273 * @param {object} panel 274 * @returns 275 */ 276 async function waitForSourceEditor(panel) { 277 return waitUntil(() => { 278 return !!panel.querySelector(".cm-editor"); 279 }); 280 } 281 282 /** 283 * Execute an input expression. 284 * 285 * @param {object} hud : The webconsole. 286 * @param {string} input : The input expression to execute. 287 */ 288 function execute(hud, input) { 289 return hud.ui.wrapper.dispatchEvaluateExpression(input); 290 } 291 292 /** 293 * Execute an input expression and wait for a message with the expected text 294 * with given message type to be displayed in the output. 295 * 296 * @param {object} hud : The webconsole. 297 * @param {string} input : The input expression to execute. 298 * @param {string} matchingText : A string that should match the message body content. 299 * @param {string} typeSelector : A part of selector for the message, to 300 * specify the message type. 301 */ 302 function executeAndWaitForMessageByType( 303 hud, 304 input, 305 matchingText, 306 typeSelector 307 ) { 308 const onMessage = waitForMessageByType(hud, matchingText, typeSelector); 309 execute(hud, input); 310 return onMessage; 311 } 312 313 /** 314 * Type-specific wrappers for executeAndWaitForMessageByType 315 * 316 * @param {object} hud : The webconsole. 317 * @param {string} input : The input expression to execute. 318 * @param {string} matchingText : A string that should match the message body 319 * content. 320 */ 321 function executeAndWaitForResultMessage(hud, input, matchingText) { 322 return executeAndWaitForMessageByType(hud, input, matchingText, ".result"); 323 } 324 325 function executeAndWaitForErrorMessage(hud, input, matchingText) { 326 return executeAndWaitForMessageByType(hud, input, matchingText, ".error"); 327 } 328 329 /** 330 * Set the input value, simulates the right keyboard event to evaluate it, 331 * depending on if the console is in editor mode or not, and wait for a message 332 * with the expected text with given message type to be displayed in the output. 333 * 334 * @param {object} hud : The webconsole. 335 * @param {string} input : The input expression to execute. 336 * @param {string} matchingText : A string that should match the message body 337 * content. 338 * @param {string} typeSelector : A part of selector for the message, to 339 * specify the message type. 340 */ 341 function keyboardExecuteAndWaitForMessageByType( 342 hud, 343 input, 344 matchingText, 345 typeSelector 346 ) { 347 hud.jsterm.focus(); 348 setInputValue(hud, input); 349 const onMessage = waitForMessageByType(hud, matchingText, typeSelector); 350 if (isEditorModeEnabled(hud)) { 351 EventUtils.synthesizeKey("KEY_Enter", { 352 [Services.appinfo.OS === "Darwin" ? "metaKey" : "ctrlKey"]: true, 353 }); 354 } else { 355 EventUtils.synthesizeKey("VK_RETURN"); 356 } 357 return onMessage; 358 } 359 360 /** 361 * Type-specific wrappers for keyboardExecuteAndWaitForMessageByType 362 * 363 * @param {object} hud : The webconsole. 364 * @param {string} input : The input expression to execute. 365 * @param {string} matchingText : A string that should match the message body 366 * content. 367 */ 368 function keyboardExecuteAndWaitForResultMessage(hud, input, matchingText) { 369 return keyboardExecuteAndWaitForMessageByType( 370 hud, 371 input, 372 matchingText, 373 ".result" 374 ); 375 } 376 377 /** 378 * Wait for a message to be logged and ensure it is logged only once. 379 * 380 * @param object hud 381 * The web console. 382 * @param string text 383 * A substring that can be found in the message. 384 * @param string typeSelector 385 * A part of selector for the message, to specify the message type. 386 * @return {Node} the node corresponding the found message 387 */ 388 async function checkUniqueMessageExists(hud, msg, typeSelector) { 389 info(`Checking "${msg}" was logged`); 390 let messages; 391 try { 392 messages = await waitFor(async () => { 393 const msgs = await findMessagesVirtualizedByType({ 394 hud, 395 text: msg, 396 typeSelector, 397 }); 398 return msgs.length ? msgs : null; 399 }); 400 } catch (e) { 401 ok(false, `Message "${msg}" wasn't logged\n`); 402 return null; 403 } 404 405 is(messages.length, 1, `"${msg}" was logged once`); 406 const [messageEl] = messages; 407 const repeatNode = messageEl.querySelector(".message-repeats"); 408 is(repeatNode, null, `"${msg}" wasn't repeated`); 409 return messageEl; 410 } 411 412 /** 413 * Simulate a context menu event on the provided element, and wait for the console context 414 * menu to open. Returns a promise that resolves the menu popup element. 415 * 416 * @param object hud 417 * The web console. 418 * @param element element 419 * The dom element on which the context menu event should be synthesized. 420 * @return promise 421 */ 422 async function openContextMenu(hud, element) { 423 const onConsoleMenuOpened = hud.ui.wrapper.once("menu-open"); 424 synthesizeContextMenuEvent(element); 425 await onConsoleMenuOpened; 426 return _getContextMenu(hud); 427 } 428 429 /** 430 * Hide the webconsole context menu popup. Returns a promise that will resolve when the 431 * context menu popup is hidden or immediately if the popup can't be found. 432 * 433 * @param object hud 434 * The web console. 435 * @return promise 436 */ 437 function hideContextMenu(hud) { 438 const popup = _getContextMenu(hud); 439 if (!popup || popup.state == "hidden") { 440 return Promise.resolve(); 441 } 442 443 const onPopupHidden = once(popup, "popuphidden"); 444 popup.hidePopup(); 445 return onPopupHidden; 446 } 447 448 function _getContextMenu(hud) { 449 const toolbox = hud.toolbox; 450 const doc = toolbox ? toolbox.topWindow.document : hud.chromeWindow.document; 451 return doc.getElementById("webconsole-menu"); 452 } 453 454 /** 455 * Toggle Enable network monitoring setting 456 * 457 * @param object hud 458 * The web console. 459 * @param boolean shouldBeSwitchedOn 460 * The expected state the setting should be in after the toggle. 461 */ 462 async function toggleNetworkMonitoringConsoleSetting(hud, shouldBeSwitchedOn) { 463 const selector = 464 ".webconsole-console-settings-menu-item-enableNetworkMonitoring"; 465 const settingChanged = waitFor(() => { 466 const el = getConsoleSettingElement(hud, selector); 467 return shouldBeSwitchedOn 468 ? el.getAttribute("aria-checked") === "true" 469 : el.getAttribute("aria-checked") !== "true"; 470 }); 471 await toggleConsoleSetting(hud, selector); 472 await settingChanged; 473 } 474 475 async function toggleConsoleSetting(hud, selector) { 476 const toolbox = hud.toolbox; 477 const doc = toolbox ? toolbox.doc : hud.chromeWindow.document; 478 479 const menuItem = doc.querySelector(selector); 480 menuItem.click(); 481 } 482 483 function getConsoleSettingElement(hud, selector) { 484 const toolbox = hud.toolbox; 485 const doc = toolbox ? toolbox.doc : hud.chromeWindow.document; 486 487 return doc.querySelector(selector); 488 } 489 490 function checkConsoleSettingState(hud, selector, enabled) { 491 const el = getConsoleSettingElement(hud, selector); 492 const checked = el.getAttribute("aria-checked") === "true"; 493 494 if (enabled) { 495 ok(checked, "setting is enabled"); 496 } else { 497 ok(!checked, "setting is disabled"); 498 } 499 } 500 501 /** 502 * Returns a promise that resolves when the node passed as an argument mutate 503 * according to the passed configuration. 504 * 505 * @param {Node} node - The node to observe mutations on. 506 * @param {object} observeConfig - A configuration object for MutationObserver.observe. 507 * @returns {Promise} 508 */ 509 function waitForNodeMutation(node, observeConfig = {}) { 510 return new Promise(resolve => { 511 const observer = new MutationObserver(mutations => { 512 resolve(mutations); 513 observer.disconnect(); 514 }); 515 observer.observe(node, observeConfig); 516 }); 517 } 518 519 /** 520 * Search for a given message. When found, simulate a click on the 521 * message's location, checking to make sure that the debugger opens 522 * the corresponding URL. If the message was generated by a logpoint, 523 * check if the corresponding logpoint editing panel is opened. 524 * 525 * @param {object} hud 526 * The webconsole 527 * @param {object} options 528 * - text: {String} The text to search for. This should be contained in 529 * the message. The searching is done with 530 * @see findMessageByType. 531 * - typeSelector: {string} A part of selector for the message, to 532 * specify the message type. 533 * - url : {String|null} URL expected to be opened. 534 * - line : {Number|null} Line expected to be opened. 535 * - column : {Number|null} Column expected to be opened. 536 * - logPointExpr: {String} The logpoint expression 537 */ 538 async function testOpenInDebugger( 539 hud, 540 { text, typeSelector, url, column, line, logPointExpr = undefined } 541 ) { 542 info(`Finding message for open-in-debugger test; text is "${text}"`); 543 const messageNode = await waitFor(() => 544 findMessageByType(hud, text, typeSelector) 545 ); 546 const locationNode = messageNode.querySelector(".message-location"); 547 ok(locationNode, "The message does have a location link"); 548 549 await clickAndAssertFrameLinkNode( 550 hud.toolbox, 551 locationNode, 552 { url, column, line }, 553 logPointExpr 554 ); 555 } 556 557 /** 558 * Returns true if the give node is currently focused. 559 */ 560 function hasFocus(node) { 561 return ( 562 node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus() 563 ); 564 } 565 566 /** 567 * Get the value of the console input . 568 * 569 * @param {WebConsole} hud: The webconsole 570 * @returns {string}: The value of the console input. 571 */ 572 function getInputValue(hud) { 573 return hud.jsterm._getValue(); 574 } 575 576 /** 577 * Set the value of the console input . 578 * 579 * @param {WebConsole} hud: The webconsole 580 * @param {string} value : The value to set the console input to. 581 */ 582 function setInputValue(hud, value) { 583 const onValueSet = hud.jsterm.once("set-input-value"); 584 hud.jsterm._setValue(value); 585 return onValueSet; 586 } 587 588 /** 589 * Set the value of the console input and its caret position, and wait for the 590 * autocompletion to be updated. 591 * 592 * @param {WebConsole} hud: The webconsole 593 * @param {string} value : The value to set the jsterm to. 594 * @param {Integer} caretPosition : The index where to place the cursor. A negative 595 * number will place the caret at (value.length - offset) position. 596 * Default to value.length (caret set at the end). 597 * @returns {Promise} resolves when the jsterm is completed. 598 */ 599 async function setInputValueForAutocompletion( 600 hud, 601 value, 602 caretPosition = value.length 603 ) { 604 const { jsterm } = hud; 605 606 const initialPromises = []; 607 if (jsterm.autocompletePopup.isOpen) { 608 initialPromises.push(jsterm.autocompletePopup.once("popup-closed")); 609 } 610 setInputValue(hud, ""); 611 await Promise.all(initialPromises); 612 613 // Wait for next tick. Tooltip tests sometimes fail to successively hide and 614 // show tooltips on Win32 debug. 615 await waitForTick(); 616 617 jsterm.focus(); 618 619 const updated = jsterm.once("autocomplete-updated"); 620 EventUtils.sendString(value, hud.iframeWindow); 621 await updated; 622 623 // Wait for next tick. Tooltip tests sometimes fail to successively hide and 624 // show tooltips on Win32 debug. 625 await waitForTick(); 626 627 if (caretPosition < 0) { 628 caretPosition = value.length + caretPosition; 629 } 630 631 if (Number.isInteger(caretPosition)) { 632 jsterm.editor.setCursor(jsterm.editor.getPosition(caretPosition)); 633 } 634 } 635 636 /** 637 * Set the value of the console input and wait for the confirm dialog to be displayed. 638 * 639 * @param {Toolbox} toolbox 640 * @param {WebConsole} hud 641 * @param {string} value : The value to set the jsterm to. 642 * Default to value.length (caret set at the end). 643 * @returns {Promise<HTMLElement>} resolves with dialog element when it is opened. 644 */ 645 async function setInputValueForGetterConfirmDialog(toolbox, hud, value) { 646 await setInputValueForAutocompletion(hud, value); 647 await waitFor(() => isConfirmDialogOpened(toolbox)); 648 ok(true, "The confirm dialog is displayed"); 649 return getConfirmDialog(toolbox); 650 } 651 652 /** 653 * Checks if the console input has the expected completion value. 654 * 655 * @param {WebConsole} hud 656 * @param {string} expectedValue 657 * @param {string} assertionInfo: Description of the assertion passed to `is`. 658 */ 659 function checkInputCompletionValue(hud, expectedValue, assertionInfo) { 660 const completionValue = getInputCompletionValue(hud); 661 if (completionValue === null) { 662 ok(false, "Couldn't retrieve the completion value"); 663 } 664 665 info(`Expects "${expectedValue}", is "${completionValue}"`); 666 is(completionValue, expectedValue, assertionInfo); 667 } 668 669 /** 670 * Checks if the cursor on console input is at expected position. 671 * 672 * @param {WebConsole} hud 673 * @param {Integer} expectedCursorIndex 674 * @param {string} assertionInfo: Description of the assertion passed to `is`. 675 */ 676 function checkInputCursorPosition(hud, expectedCursorIndex, assertionInfo) { 677 const { jsterm } = hud; 678 is(jsterm.editor.getCursor().ch, expectedCursorIndex, assertionInfo); 679 } 680 681 /** 682 * Checks the console input value and the cursor position given an expected string 683 * containing a "|" to indicate the expected cursor position. 684 * 685 * @param {WebConsole} hud 686 * @param {string} expectedStringWithCursor: 687 * String with a "|" to indicate the expected cursor position. 688 * For example, this is how you assert an empty value with the focus "|", 689 * and this indicates the value should be "test" and the cursor at the 690 * end of the input: "test|". 691 * @param {string} assertionInfo: Description of the assertion passed to `is`. 692 */ 693 function checkInputValueAndCursorPosition( 694 hud, 695 expectedStringWithCursor, 696 assertionInfo 697 ) { 698 info(`Checking jsterm state: \n${expectedStringWithCursor}`); 699 if (!expectedStringWithCursor.includes("|")) { 700 ok( 701 false, 702 `expectedStringWithCursor must contain a "|" char to indicate cursor position` 703 ); 704 } 705 706 const inputValue = expectedStringWithCursor.replace("|", ""); 707 const { jsterm } = hud; 708 709 is(getInputValue(hud), inputValue, "console input has expected value"); 710 const lines = expectedStringWithCursor.split("\n"); 711 const lineWithCursor = lines.findIndex(line => line.includes("|")); 712 const { ch, line } = jsterm.editor.getCursor(); 713 is(line, lineWithCursor, assertionInfo + " - correct line"); 714 is(ch, lines[lineWithCursor].indexOf("|"), assertionInfo + " - correct ch"); 715 } 716 717 /** 718 * Returns the console input completion value. 719 * 720 * @param {WebConsole} hud 721 * @returns {string} 722 */ 723 function getInputCompletionValue(hud) { 724 const { jsterm } = hud; 725 return jsterm.editor.getAutoCompletionText(); 726 } 727 728 function closeAutocompletePopup(hud) { 729 const { jsterm } = hud; 730 731 if (!jsterm.autocompletePopup.isOpen) { 732 return Promise.resolve(); 733 } 734 735 const onPopupClosed = jsterm.autocompletePopup.once("popup-closed"); 736 const onAutocompleteUpdated = jsterm.once("autocomplete-updated"); 737 EventUtils.synthesizeKey("KEY_Escape"); 738 return Promise.all([onPopupClosed, onAutocompleteUpdated]); 739 } 740 741 /** 742 * Returns a boolean indicating if the console input is focused. 743 * 744 * @param {WebConsole} hud 745 * @returns {boolean} 746 */ 747 function isInputFocused(hud) { 748 const { jsterm } = hud; 749 const document = hud.ui.outputNode.ownerDocument; 750 const documentIsFocused = document.hasFocus(); 751 return documentIsFocused && jsterm.editor.hasFocus(); 752 } 753 754 /** 755 * Open the JavaScript debugger. 756 * 757 * @param object options 758 * Options for opening the debugger: 759 * - tab: the tab you want to open the debugger for. 760 * @return object 761 * A promise that is resolved once the debugger opens, or rejected if 762 * the open fails. The resolution callback is given one argument, an 763 * object that holds the following properties: 764 * - target: the Target object for the Tab. 765 * - toolbox: the Toolbox instance. 766 * - panel: the jsdebugger panel instance. 767 */ 768 async function openDebugger(options = {}) { 769 if (!options.tab) { 770 options.tab = gBrowser.selectedTab; 771 } 772 773 let toolbox = gDevTools.getToolboxForTab(options.tab); 774 const dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger"); 775 if (dbgPanelAlreadyOpen) { 776 await toolbox.selectTool("jsdebugger"); 777 778 return { 779 target: toolbox.target, 780 toolbox, 781 panel: toolbox.getCurrentPanel(), 782 }; 783 } 784 785 toolbox = await gDevTools.showToolboxForTab(options.tab, { 786 toolId: "jsdebugger", 787 }); 788 const panel = toolbox.getCurrentPanel(); 789 790 await toolbox.threadFront.getSources(); 791 792 return { target: toolbox.target, toolbox, panel }; 793 } 794 795 async function openInspector(options = {}) { 796 if (!options.tab) { 797 options.tab = gBrowser.selectedTab; 798 } 799 800 const toolbox = await gDevTools.showToolboxForTab(options.tab, { 801 toolId: "inspector", 802 }); 803 804 return toolbox.getCurrentPanel(); 805 } 806 807 /** 808 * Open the netmonitor for the given tab, or the current one if none given. 809 * 810 * @param Element tab 811 * Optional tab element for which you want open the netmonitor. 812 * Defaults to current selected tab. 813 * @return Promise 814 * A promise that is resolved with the netmonitor panel once the netmonitor is open. 815 */ 816 async function openNetMonitor(tab) { 817 tab = tab || gBrowser.selectedTab; 818 let toolbox = gDevTools.getToolboxForTab(tab); 819 if (!toolbox) { 820 toolbox = await gDevTools.showToolboxForTab(tab); 821 } 822 await toolbox.selectTool("netmonitor"); 823 return toolbox.getCurrentPanel(); 824 } 825 826 /** 827 * Open the Web Console for the given tab, or the current one if none given. 828 * 829 * @param Element tab 830 * Optional tab element for which you want open the Web Console. 831 * Defaults to current selected tab. 832 * @return Promise 833 * A promise that is resolved with the console hud once the web console is open. 834 */ 835 async function openConsole(tab) { 836 tab = tab || gBrowser.selectedTab; 837 const toolbox = await gDevTools.showToolboxForTab(tab, { 838 toolId: "webconsole", 839 }); 840 return toolbox.getCurrentPanel().hud; 841 } 842 843 /** 844 * Close the Web Console for the given tab. 845 * 846 * @param Element [tab] 847 * Optional tab element for which you want close the Web Console. 848 * Defaults to current selected tab. 849 * @return object 850 * A promise that is resolved once the web console is closed. 851 */ 852 async function closeConsole(tab = gBrowser.selectedTab) { 853 const toolbox = gDevTools.getToolboxForTab(tab); 854 if (toolbox) { 855 await toolbox.destroy(); 856 } 857 } 858 859 /** 860 * Open a network request logged in the webconsole in the netmonitor panel. 861 * 862 * @param {object} toolbox 863 * @param {object} hud 864 * @param {string} url 865 * URL of the request as logged in the netmonitor. 866 * @param {string} urlInConsole 867 * (optional) Use if the logged URL in webconsole is different from the real URL. 868 */ 869 async function openMessageInNetmonitor(toolbox, hud, url, urlInConsole) { 870 // By default urlInConsole should be the same as the complete url. 871 urlInConsole = urlInConsole || url; 872 873 const message = await waitFor(() => 874 findMessageByType(hud, urlInConsole, ".network") 875 ); 876 877 const onNetmonitorSelected = toolbox.once( 878 "netmonitor-selected", 879 (event, panel) => { 880 return panel; 881 } 882 ); 883 884 const menuPopup = await openContextMenu(hud, message); 885 const openInNetMenuItem = menuPopup.querySelector( 886 "#console-menu-open-in-network-panel" 887 ); 888 ok(openInNetMenuItem, "open in network panel item is enabled"); 889 menuPopup.activateItem(openInNetMenuItem); 890 891 const { panelWin } = await onNetmonitorSelected; 892 ok( 893 true, 894 "The netmonitor panel is selected when clicking on the network message" 895 ); 896 897 const { store, windowRequire } = panelWin; 898 const nmActions = windowRequire( 899 "devtools/client/netmonitor/src/actions/index" 900 ); 901 const { getSelectedRequest } = windowRequire( 902 "devtools/client/netmonitor/src/selectors/index" 903 ); 904 905 store.dispatch(nmActions.batchEnable(false)); 906 907 await waitFor(() => { 908 const selected = getSelectedRequest(store.getState()); 909 return selected && selected.url === url; 910 }, `network entry for the URL "${url}" wasn't found`); 911 912 ok(true, "The attached url is correct."); 913 914 info( 915 "Wait for the netmonitor headers panel to appear as it spawns RDP requests" 916 ); 917 await waitFor(() => 918 panelWin.document.querySelector("#headers-panel .headers-overview") 919 ); 920 } 921 922 function selectNode(hud, node) { 923 const outputContainer = hud.ui.outputNode.querySelector(".webconsole-output"); 924 925 // We must first blur the input or else we can't select anything. 926 outputContainer.ownerDocument.activeElement.blur(); 927 928 const selection = outputContainer.ownerDocument.getSelection(); 929 const range = document.createRange(); 930 range.selectNodeContents(node); 931 selection.removeAllRanges(); 932 selection.addRange(range); 933 934 return selection; 935 } 936 937 async function waitForBrowserConsole() { 938 return new Promise(resolve => { 939 Services.obs.addObserver(function observer(subject) { 940 Services.obs.removeObserver(observer, "web-console-created"); 941 subject.QueryInterface(Ci.nsISupportsString); 942 943 const hud = BrowserConsoleManager.getBrowserConsole(); 944 ok(hud, "browser console is open"); 945 is(subject.data, hud.hudId, "notification hudId is correct"); 946 947 executeSoon(() => resolve(hud)); 948 }, "web-console-created"); 949 }); 950 } 951 952 /** 953 * Get the state of a console filter. 954 * 955 * @param {object} hud 956 */ 957 async function getFilterState(hud) { 958 const { outputNode } = hud.ui; 959 const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary"); 960 const buttons = filterBar.querySelectorAll("button"); 961 const result = {}; 962 963 for (const button of buttons) { 964 result[button.dataset.category] = 965 button.getAttribute("aria-pressed") === "true"; 966 } 967 968 return result; 969 } 970 971 /** 972 * Return the filter input element. 973 * 974 * @param {object} hud 975 * @return {HTMLInputElement} 976 */ 977 function getFilterInput(hud) { 978 return hud.ui.outputNode.querySelector(".devtools-searchbox input"); 979 } 980 981 /** 982 * Set the state of a console filter. 983 * 984 * @param {object} hud 985 * @param {object} settings 986 * Category settings in the following format: 987 * { 988 * error: true, 989 * warn: true, 990 * log: true, 991 * info: true, 992 * debug: true, 993 * css: false, 994 * netxhr: false, 995 * net: false, 996 * text: "" 997 * } 998 */ 999 async function setFilterState(hud, settings) { 1000 const { outputNode } = hud.ui; 1001 const filterBar = outputNode.querySelector(".webconsole-filterbar-secondary"); 1002 1003 for (const category in settings) { 1004 const value = settings[category]; 1005 const button = filterBar.querySelector(`[data-category="${category}"]`); 1006 1007 if (category === "text") { 1008 const filterInput = getFilterInput(hud); 1009 filterInput.focus(); 1010 filterInput.select(); 1011 const win = outputNode.ownerDocument.defaultView; 1012 if (!value) { 1013 EventUtils.synthesizeKey("KEY_Delete", {}, win); 1014 } else { 1015 EventUtils.sendString(value, win); 1016 } 1017 await waitFor(() => filterInput.value === value); 1018 continue; 1019 } 1020 1021 if (!button) { 1022 ok( 1023 false, 1024 `setFilterState() called with a category of ${category}, ` + 1025 `which doesn't exist.` 1026 ); 1027 } 1028 1029 info( 1030 `Setting the ${category} category to ${value ? "checked" : "disabled"}` 1031 ); 1032 1033 const isPressed = button.getAttribute("aria-pressed"); 1034 1035 if ((!value && isPressed === "true") || (value && isPressed !== "true")) { 1036 button.click(); 1037 1038 await waitFor(() => { 1039 const pressed = button.getAttribute("aria-pressed"); 1040 if (!value) { 1041 return pressed === "false" || pressed === null; 1042 } 1043 return pressed === "true"; 1044 }); 1045 } 1046 } 1047 } 1048 1049 /** 1050 * Reset the filters at the end of a test that has changed them. This is 1051 * important when using the `--verify` test option as when it is used you need 1052 * to manually reset the filters. 1053 * 1054 * The css, netxhr and net filters are disabled by default. 1055 * 1056 * @param {object} hud 1057 */ 1058 async function resetFilters(hud) { 1059 info("Resetting filters to their default state"); 1060 1061 const store = hud.ui.wrapper.getStore(); 1062 store.dispatch(wcActions.filtersClear()); 1063 } 1064 1065 /** 1066 * Open the reverse search input by simulating the appropriate keyboard shortcut. 1067 * 1068 * @param {object} hud 1069 * @returns {DOMNode} The reverse search dom node. 1070 */ 1071 async function openReverseSearch(hud) { 1072 info("Open the reverse search UI with a keyboard shortcut"); 1073 const onReverseSearchUiOpen = waitFor(() => getReverseSearchElement(hud)); 1074 const isMacOS = AppConstants.platform === "macosx"; 1075 if (isMacOS) { 1076 EventUtils.synthesizeKey("r", { ctrlKey: true }); 1077 } else { 1078 EventUtils.synthesizeKey("VK_F9"); 1079 } 1080 1081 const element = await onReverseSearchUiOpen; 1082 return element; 1083 } 1084 1085 function getReverseSearchElement(hud) { 1086 const { outputNode } = hud.ui; 1087 return outputNode.querySelector(".reverse-search"); 1088 } 1089 1090 function getReverseSearchInfoElement(hud) { 1091 const reverseSearchElement = getReverseSearchElement(hud); 1092 if (!reverseSearchElement) { 1093 return null; 1094 } 1095 1096 return reverseSearchElement.querySelector(".reverse-search-info"); 1097 } 1098 1099 /** 1100 * Returns a boolean indicating if the reverse search input is focused. 1101 * 1102 * @param {WebConsole} hud 1103 * @returns {boolean} 1104 */ 1105 function isReverseSearchInputFocused(hud) { 1106 const { outputNode } = hud.ui; 1107 const document = outputNode.ownerDocument; 1108 const documentIsFocused = document.hasFocus(); 1109 const reverseSearchInput = outputNode.querySelector(".reverse-search-input"); 1110 1111 return document.activeElement == reverseSearchInput && documentIsFocused; 1112 } 1113 1114 function getEagerEvaluationElement(hud) { 1115 return hud.ui.outputNode.querySelector(".eager-evaluation-result"); 1116 } 1117 1118 async function waitForEagerEvaluationResult(hud, text) { 1119 await waitUntil(() => { 1120 const elem = getEagerEvaluationElement(hud); 1121 if (elem) { 1122 if (text instanceof RegExp) { 1123 return text.test(elem.innerText); 1124 } 1125 return elem.innerText == text; 1126 } 1127 return false; 1128 }); 1129 ok(true, `Got eager evaluation result ${text}`); 1130 } 1131 1132 // This just makes sure the eager evaluation result disappears. This will pass 1133 // even for inputs which eventually have a result because nothing will be shown 1134 // while the evaluation happens. Waiting here does make sure that a previous 1135 // input was processed and sent down to the server for evaluating. 1136 async function waitForNoEagerEvaluationResult(hud) { 1137 await waitUntil(() => { 1138 const elem = getEagerEvaluationElement(hud); 1139 return elem && elem.innerText == ""; 1140 }); 1141 ok(true, `Eager evaluation result disappeared`); 1142 } 1143 1144 /** 1145 * Selects a node in the inspector. 1146 * 1147 * @param {object} toolbox 1148 * @param {string} selector: The selector for the node we want to select. 1149 */ 1150 async function selectNodeWithPicker(toolbox, selector) { 1151 const inspector = toolbox.getPanel("inspector"); 1152 1153 const onPickerStarted = toolbox.nodePicker.once("picker-started"); 1154 toolbox.nodePicker.start(); 1155 await onPickerStarted; 1156 1157 info( 1158 `Picker mode started, now clicking on "${selector}" to select that node` 1159 ); 1160 const onPickerStopped = toolbox.nodePicker.once("picker-stopped"); 1161 const onInspectorUpdated = inspector.once("inspector-updated"); 1162 1163 await safeSynthesizeMouseEventAtCenterInContentPage(selector); 1164 1165 await onPickerStopped; 1166 await onInspectorUpdated; 1167 } 1168 1169 /** 1170 * Clicks on the arrow of a single object inspector node if it exists. 1171 * 1172 * @param {HTMLElement} node: Object inspector node (.tree-node) 1173 */ 1174 async function expandObjectInspectorNode(node) { 1175 if (!node.classList.contains("tree-node")) { 1176 ok(false, "Node should be a .tree-node"); 1177 return; 1178 } 1179 const arrow = getObjectInspectorNodeArrow(node); 1180 if (!arrow) { 1181 ok(false, "Node can't be expanded"); 1182 return; 1183 } 1184 if (arrow.classList.contains("open")) { 1185 ok(false, "Node already expanded"); 1186 return; 1187 } 1188 const isLongString = node.querySelector(".node > .objectBox-string"); 1189 let onMutation; 1190 let textContentBeforeExpand; 1191 if (!isLongString) { 1192 const objectInspector = node.closest(".object-inspector"); 1193 onMutation = waitForNodeMutation(objectInspector, { 1194 childList: true, 1195 }); 1196 } else { 1197 textContentBeforeExpand = node.textContent; 1198 } 1199 arrow.click(); 1200 1201 // Long strings are not going to be expanded into children element. 1202 // Instead the tree node will update itself to show the long string. 1203 // So that we can't wait for the childList mutation. 1204 if (isLongString) { 1205 // Reps will expand on click... 1206 await waitFor(() => arrow.classList.contains("open")); 1207 // ...but it will fetch the long string content asynchronously after having expanded the TreeNode. 1208 // So also wait for the string to be updated and be longer. 1209 await waitFor( 1210 () => node.textContent.length > textContentBeforeExpand.length 1211 ); 1212 } else { 1213 await onMutation; 1214 // Waiting for the object inspector mutation isn't enough, 1215 // also wait for the children element, with higher aria-level to be added to the DOM. 1216 await waitFor(() => !!getObjectInspectorChildrenNodes(node).length); 1217 } 1218 1219 ok( 1220 arrow.classList.contains("open"), 1221 "The arrow of the root node of the tree is expanded after clicking on it" 1222 ); 1223 } 1224 1225 /** 1226 * Retrieve the arrow of a single object inspector node. 1227 * 1228 * @param {HTMLElement} node: Object inspector node (.tree-node) 1229 * @return {HTMLElement|null} the arrow element 1230 */ 1231 function getObjectInspectorNodeArrow(node) { 1232 return node.querySelector(".theme-twisty"); 1233 } 1234 1235 /** 1236 * Check if a single object inspector node is expandable. 1237 * 1238 * @param {HTMLElement} node: Object inspector node (.tree-node) 1239 * @return {boolean} true if the node can be expanded 1240 */ 1241 function isObjectInspectorNodeExpandable(node) { 1242 return !!getObjectInspectorNodeArrow(node); 1243 } 1244 1245 /** 1246 * Retrieve the nodes for a given object inspector element. 1247 * 1248 * @param {HTMLElement} oi: Object inspector element 1249 * @return {NodeList} the object inspector nodes 1250 */ 1251 function getObjectInspectorNodes(oi) { 1252 return oi.querySelectorAll(".tree-node"); 1253 } 1254 1255 /** 1256 * Retrieve the "children" nodes for a given object inspector node. 1257 * 1258 * @param {HTMLElement} node: Object inspector node (.tree-node) 1259 * @return {Array<HTMLElement>} the direct children (i.e. the ones that are one level 1260 * deeper than the passed node) 1261 */ 1262 function getObjectInspectorChildrenNodes(node) { 1263 const getLevel = n => parseInt(n.getAttribute("aria-level") || "0", 10); 1264 const level = getLevel(node); 1265 const childLevel = level + 1; 1266 const children = []; 1267 1268 let currentNode = node; 1269 while ( 1270 currentNode.nextSibling && 1271 getLevel(currentNode.nextSibling) === childLevel 1272 ) { 1273 currentNode = currentNode.nextSibling; 1274 children.push(currentNode); 1275 } 1276 1277 return children; 1278 } 1279 1280 /** 1281 * Retrieve the invoke getter button for a given object inspector node. 1282 * 1283 * @param {HTMLElement} node: Object inspector node (.tree-node) 1284 * @return {HTMLElement|null} the invoke button element 1285 */ 1286 function getObjectInspectorInvokeGetterButton(node) { 1287 return node.querySelector(".invoke-getter"); 1288 } 1289 1290 /** 1291 * Retrieve the first node that match the passed node label, for a given object inspector 1292 * element. 1293 * 1294 * @param {HTMLElement} oi: Object inspector element 1295 * @param {string} nodeLabel: label of the searched node 1296 * @return {HTMLElement|null} the Object inspector node with the matching label 1297 */ 1298 function findObjectInspectorNode(oi, nodeLabel) { 1299 return [...oi.querySelectorAll(".tree-node")].find(node => { 1300 const label = node.querySelector(".object-label"); 1301 if (!label) { 1302 return false; 1303 } 1304 return label.textContent === nodeLabel; 1305 }); 1306 } 1307 1308 /** 1309 * Return an array of the label of the autocomplete popup items. 1310 * 1311 * @param {AutocompletPopup} popup 1312 * @returns {Array<string>} 1313 */ 1314 function getAutocompletePopupLabels(popup) { 1315 return popup.getItems().map(item => item.label); 1316 } 1317 1318 /** 1319 * Check if the retrieved list of autocomplete labels of the specific popup 1320 * includes all of the expected labels. 1321 * 1322 * @param {AutocompletPopup} popup 1323 * @param {Array<string>} expected the array of expected labels 1324 */ 1325 function hasExactPopupLabels(popup, expected) { 1326 return hasPopupLabels(popup, expected, true); 1327 } 1328 1329 /** 1330 * Check if the expected label is included in the list of autocomplete labels 1331 * of the specific popup. 1332 * 1333 * @param {AutocompletPopup} popup 1334 * @param {string} label the label to check 1335 */ 1336 function hasPopupLabel(popup, label) { 1337 return hasPopupLabels(popup, [label]); 1338 } 1339 1340 /** 1341 * Validate the expected labels against the autocomplete labels. 1342 * 1343 * @param {AutocompletPopup} popup 1344 * @param {Array<string>} expectedLabels 1345 * @param {boolean} checkAll 1346 */ 1347 function hasPopupLabels(popup, expectedLabels, checkAll = false) { 1348 const autocompleteLabels = getAutocompletePopupLabels(popup); 1349 if (checkAll) { 1350 return ( 1351 autocompleteLabels.length === expectedLabels.length && 1352 autocompleteLabels.every((autoLabel, idx) => { 1353 return expectedLabels.indexOf(autoLabel) === idx; 1354 }) 1355 ); 1356 } 1357 return expectedLabels.every(expectedLabel => { 1358 return autocompleteLabels.includes(expectedLabel); 1359 }); 1360 } 1361 1362 /** 1363 * Return the "Confirm Dialog" element. 1364 * 1365 * @param toolbox 1366 * @returns {HTMLElement|null} 1367 */ 1368 function getConfirmDialog(toolbox) { 1369 const { doc } = toolbox; 1370 return doc.querySelector(".invoke-confirm"); 1371 } 1372 1373 /** 1374 * Returns true if the Confirm Dialog is opened. 1375 * 1376 * @param toolbox 1377 * @returns {boolean} 1378 */ 1379 function isConfirmDialogOpened(toolbox) { 1380 const tooltip = getConfirmDialog(toolbox); 1381 if (!tooltip) { 1382 return false; 1383 } 1384 1385 return tooltip.classList.contains("tooltip-visible"); 1386 } 1387 1388 async function selectFrame(dbg, frame) { 1389 const onScopes = waitForDispatch(dbg.store, "ADD_SCOPES"); 1390 await dbg.actions.selectFrame(frame); 1391 await onScopes; 1392 } 1393 1394 async function pauseDebugger(dbg, options) { 1395 info("Waiting for debugger to pause"); 1396 const onPaused = waitForPaused(dbg, null, options); 1397 SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { 1398 content.wrappedJSObject.firstCall(); 1399 }).catch(() => {}); 1400 await onPaused; 1401 } 1402 1403 /** 1404 * Check that the passed HTMLElement vertically overflows. 1405 * 1406 * @param {HTMLElement} container 1407 * @returns {boolean} 1408 */ 1409 function hasVerticalOverflow(container) { 1410 return container.scrollHeight > container.clientHeight; 1411 } 1412 1413 /** 1414 * Check that the passed HTMLElement is scrolled to the bottom. 1415 * 1416 * @param {HTMLElement} container 1417 * @returns {boolean} 1418 */ 1419 function isScrolledToBottom(container) { 1420 if (!container.lastChild) { 1421 return true; 1422 } 1423 const lastNodeHeight = container.lastChild.clientHeight; 1424 return ( 1425 container.scrollTop + container.clientHeight >= 1426 container.scrollHeight - lastNodeHeight / 2 1427 ); 1428 } 1429 1430 /** 1431 * 1432 * @param {WebConsole} hud 1433 * @param {Array<string>} expectedMessages: An array of string representing the messages 1434 * from the output. This can only be a part of the string of the 1435 * message. 1436 * Start the string with "▶︎⚠" or "▼⚠" to indicate that the 1437 * message is a warningGroup (with respectively an open or 1438 * collapsed arrow). 1439 * Start the string with "|︎ " to indicate that the message is 1440 * inside a group and should be indented. 1441 */ 1442 async function checkConsoleOutputForWarningGroup(hud, expectedMessages) { 1443 const messages = await findAllMessagesVirtualized(hud); 1444 is( 1445 messages.length, 1446 expectedMessages.length, 1447 "Got the expected number of messages" 1448 ); 1449 1450 const isInWarningGroup = index => { 1451 const message = expectedMessages[index]; 1452 if (!message.startsWith("|")) { 1453 return false; 1454 } 1455 const groups = expectedMessages 1456 .slice(0, index) 1457 .reverse() 1458 .filter(m => !m.startsWith("|")); 1459 if (groups.length === 0) { 1460 ok(false, "Unexpected structure: an indented message isn't in a group"); 1461 } 1462 1463 return groups[0].startsWith("▼︎⚠"); 1464 }; 1465 1466 for (let [i, expectedMessage] of expectedMessages.entries()) { 1467 // Refresh the reference to the message, as it may have been scrolled out of existence. 1468 const message = await findMessageVirtualizedById({ 1469 hud, 1470 messageId: messages[i].getAttribute("data-message-id"), 1471 }); 1472 info(`Checking "${expectedMessage}"`); 1473 1474 // Collapsed Warning group 1475 if (expectedMessage.startsWith("▶︎⚠")) { 1476 is( 1477 message.querySelector(".arrow").getAttribute("aria-expanded"), 1478 "false", 1479 "There's a collapsed arrow" 1480 ); 1481 is( 1482 message.getAttribute("data-indent"), 1483 "0", 1484 "The warningGroup has the expected indent" 1485 ); 1486 expectedMessage = expectedMessage.replace("▶︎⚠", ""); 1487 } 1488 1489 // Expanded Warning group 1490 if (expectedMessage.startsWith("▼︎⚠")) { 1491 is( 1492 message.querySelector(".arrow").getAttribute("aria-expanded"), 1493 "true", 1494 "There's an expanded arrow" 1495 ); 1496 is( 1497 message.getAttribute("data-indent"), 1498 "0", 1499 "The warningGroup has the expected indent" 1500 ); 1501 expectedMessage = expectedMessage.replace("▼︎⚠", ""); 1502 } 1503 1504 // Collapsed console.group 1505 if (expectedMessage.startsWith("▶︎")) { 1506 is( 1507 message.querySelector(".arrow").getAttribute("aria-expanded"), 1508 "false", 1509 "There's a collapsed arrow" 1510 ); 1511 expectedMessage = expectedMessage.replace("▶︎ ", ""); 1512 } 1513 1514 // Expanded console.group 1515 if (expectedMessage.startsWith("▼")) { 1516 is( 1517 message.querySelector(".arrow").getAttribute("aria-expanded"), 1518 "true", 1519 "There's an expanded arrow" 1520 ); 1521 expectedMessage = expectedMessage.replace("▼ ", ""); 1522 } 1523 1524 // In-group message 1525 if (expectedMessage.startsWith("|")) { 1526 if (isInWarningGroup(i)) { 1527 ok( 1528 message.querySelector(".warning-indent"), 1529 "The message has the expected indent" 1530 ); 1531 } 1532 1533 expectedMessage = expectedMessage.replace("| ", ""); 1534 } else { 1535 is( 1536 message.getAttribute("data-indent"), 1537 "0", 1538 "The message has the expected indent" 1539 ); 1540 } 1541 1542 ok( 1543 message.textContent.trim().includes(expectedMessage.trim()), 1544 `Message includes ` + 1545 `the expected "${expectedMessage}" content - "${message.textContent.trim()}"` 1546 ); 1547 } 1548 } 1549 1550 /** 1551 * Check that there is a message with the specified text that has the specified 1552 * stack information. Self-hosted frames are ignored. 1553 * 1554 * @param {WebConsole} hud 1555 * @param {string} text 1556 * message substring to look for 1557 * @param {Array<number>} expectedFrameLines 1558 * line numbers of the frames expected in the stack 1559 */ 1560 async function checkMessageStack(hud, text, expectedFrameLines) { 1561 info(`Checking message stack for "${text}"`); 1562 const msgNode = await waitFor( 1563 () => findErrorMessage(hud, text), 1564 `Couln't find message including "${text}"` 1565 ); 1566 ok(!msgNode.classList.contains("open"), `Error logged not expanded`); 1567 1568 const button = await waitFor( 1569 () => msgNode.querySelector(".collapse-button"), 1570 `Couldn't find the expand button on "${text}" message` 1571 ); 1572 button.click(); 1573 1574 const framesNode = await waitFor( 1575 () => msgNode.querySelector(".message-body-wrapper > .stacktrace .frames"), 1576 `Couldn't find stacktrace frames on "${text}" message` 1577 ); 1578 const frameNodes = Array.from(framesNode.querySelectorAll(".frame")).filter( 1579 el => { 1580 const fileName = el.querySelector(".filename").textContent; 1581 return ( 1582 fileName !== "self-hosted" && 1583 !fileName.startsWith("chrome:") && 1584 !fileName.startsWith("resource:") 1585 ); 1586 } 1587 ); 1588 1589 for (let i = 0; i < frameNodes.length; i++) { 1590 const frameNode = frameNodes[i]; 1591 is( 1592 frameNode.querySelector(".line").textContent, 1593 expectedFrameLines[i].toString(), 1594 `Found line ${expectedFrameLines[i]} for frame #${i}` 1595 ); 1596 } 1597 1598 is( 1599 frameNodes.length, 1600 expectedFrameLines.length, 1601 `Found ${frameNodes.length} frames` 1602 ); 1603 } 1604 1605 /** 1606 * Reload the content page. 1607 * 1608 * @returns {Promise} A promise that will return when the page is fully loaded (i.e., the 1609 * `load` event was fired). 1610 */ 1611 function reloadPage() { 1612 const onLoad = BrowserTestUtils.waitForContentEvent( 1613 gBrowser.selectedBrowser, 1614 "load", 1615 true 1616 ); 1617 SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 1618 content.location.reload(); 1619 }); 1620 return onLoad; 1621 } 1622 1623 /** 1624 * Check if the editor mode is enabled (i.e. .webconsole-app has the expected class). 1625 * 1626 * @param {WebConsole} hud 1627 * @returns {boolean} 1628 */ 1629 function isEditorModeEnabled(hud) { 1630 const { outputNode } = hud.ui; 1631 const appNode = outputNode.querySelector(".webconsole-app"); 1632 return appNode.classList.contains("jsterm-editor"); 1633 } 1634 1635 /** 1636 * Toggle the layout between in-line and editor. 1637 * 1638 * @param {WebConsole} hud 1639 * @returns {Promise} A promise that resolves once the layout change was rendered. 1640 */ 1641 function toggleLayout(hud) { 1642 const isMacOS = Services.appinfo.OS === "Darwin"; 1643 const enabled = isEditorModeEnabled(hud); 1644 1645 EventUtils.synthesizeKey("b", { 1646 [isMacOS ? "metaKey" : "ctrlKey"]: true, 1647 }); 1648 return waitFor(() => isEditorModeEnabled(hud) === !enabled); 1649 } 1650 1651 /** 1652 * Wait until all lazily fetch requests in netmonitor get finished. 1653 * Otherwise test will be shutdown too early and cause failure. 1654 */ 1655 async function waitForLazyRequests(toolbox) { 1656 const ui = toolbox.getCurrentPanel().hud.ui; 1657 return waitUntil(() => { 1658 return ( 1659 !ui.networkDataProvider.lazyRequestData.size && 1660 // Make sure that batched request updates are all complete 1661 // as they trigger late lazy data requests. 1662 !ui.wrapper.queuedRequestUpdates.length 1663 ); 1664 }); 1665 } 1666 1667 /** 1668 * Clear the console output and wait for eventual object actors to be released. 1669 * 1670 * @param {WebConsole} hud 1671 * @param {object} An options object with the following properties: 1672 * - {Boolean} keepStorage: true to prevent clearing the messages storage. 1673 */ 1674 async function clearOutput(hud, { keepStorage = false } = {}) { 1675 const { ui } = hud; 1676 const promises = [ui.once("messages-cleared")]; 1677 1678 // If there's an object inspector, we need to wait for the actors to be released. 1679 if (ui.outputNode.querySelector(".object-inspector")) { 1680 promises.push(ui.once("fronts-released")); 1681 } 1682 1683 ui.clearOutput(!keepStorage); 1684 await Promise.all(promises); 1685 } 1686 1687 /** 1688 * Retrieve all the items of the context selector menu. 1689 * 1690 * @param {WebConsole} hud 1691 * @return Array<Element> 1692 */ 1693 function getContextSelectorItems(hud) { 1694 const toolbox = hud.toolbox; 1695 const doc = toolbox ? toolbox.doc : hud.chromeWindow.document; 1696 const list = doc.getElementById( 1697 "webconsole-console-evaluation-context-selector-menu-list" 1698 ); 1699 return Array.from(list.querySelectorAll("li.menuitem button, hr")); 1700 } 1701 1702 /** 1703 * Check that the evaluation context selector menu has the expected item, in the expected 1704 * state. 1705 * 1706 * @param {WebConsole} hud 1707 * @param {Array<object>} expected: An array of object (see checkContextSelectorMenuItemAt 1708 * for expected properties) 1709 */ 1710 function checkContextSelectorMenu(hud, expected) { 1711 const items = getContextSelectorItems(hud); 1712 1713 is( 1714 items.length, 1715 expected.length, 1716 "The context selector menu has the expected number of items" 1717 ); 1718 1719 expected.forEach((expectedItem, i) => { 1720 checkContextSelectorMenuItemAt(hud, i, expectedItem); 1721 }); 1722 } 1723 1724 /** 1725 * Check that the evaluation context selector menu has the expected item at the specified index. 1726 * 1727 * @param {WebConsole} hud 1728 * @param {number} index 1729 * @param {object} expected 1730 * @param {string} expected.label: The label of the target 1731 * @param {string} expected.tooltip: The tooltip of the target element in the menu 1732 * @param {boolean} expected.checked: if the target should be selected or not 1733 * @param {boolean} expected.separator: if the element is a simple separator 1734 * @param {boolean} expected.indented: if the element is indented 1735 */ 1736 function checkContextSelectorMenuItemAt(hud, index, expected) { 1737 const el = getContextSelectorItems(hud).at(index); 1738 1739 if (expected.separator === true) { 1740 is(el.getAttribute("role"), "menuseparator", "The element is a separator"); 1741 return; 1742 } 1743 1744 const elChecked = el.getAttribute("aria-checked") === "true"; 1745 const elTooltip = el.getAttribute("title"); 1746 const elLabel = el.querySelector(".label").innerText; 1747 const indented = el.classList.contains("indented"); 1748 1749 is(elLabel, expected.label, `The item has the expected label`); 1750 is(elTooltip, expected.tooltip, `Item "${elLabel}" has the expected tooltip`); 1751 is( 1752 elChecked, 1753 expected.checked, 1754 `Item "${elLabel}" is ${expected.checked ? "checked" : "unchecked"}` 1755 ); 1756 is( 1757 indented, 1758 expected.indented ?? false, 1759 `Item "${elLabel}" is ${!indented ? " not" : ""} indented` 1760 ); 1761 } 1762 1763 /** 1764 * Select a target in the context selector. 1765 * 1766 * @param {WebConsole} hud 1767 * @param {string} targetLabel: The label of the target to select. 1768 */ 1769 function selectTargetInContextSelector(hud, targetLabel) { 1770 const items = getContextSelectorItems(hud); 1771 const itemToSelect = items.find( 1772 item => item.querySelector(".label")?.innerText === targetLabel 1773 ); 1774 if (!itemToSelect) { 1775 ok(false, `Couldn't find target with "${targetLabel}" label`); 1776 return; 1777 } 1778 1779 itemToSelect.click(); 1780 } 1781 1782 /** 1783 * A helper that returns the size of the image that was just put into the clipboard by the 1784 * :screenshot command. 1785 * 1786 * @return The {width, height} dimension object. 1787 */ 1788 async function getImageSizeFromClipboard() { 1789 const clipid = Ci.nsIClipboard; 1790 const clip = Cc["@mozilla.org/widget/clipboard;1"].getService(clipid); 1791 const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( 1792 Ci.nsITransferable 1793 ); 1794 const flavor = "image/png"; 1795 trans.init(null); 1796 trans.addDataFlavor(flavor); 1797 1798 clip.getData( 1799 trans, 1800 clipid.kGlobalClipboard, 1801 SpecialPowers.wrap(window).browsingContext.currentWindowContext 1802 ); 1803 const data = {}; 1804 trans.getTransferData(flavor, data); 1805 1806 ok(data.value, "screenshot exists"); 1807 1808 let image = data.value; 1809 1810 // Due to the differences in how images could be stored in the clipboard the 1811 // checks below are needed. The clipboard could already provide the image as 1812 // byte streams or as image container. If it's not possible obtain a 1813 // byte stream, the function throws. 1814 1815 if (image instanceof Ci.imgIContainer) { 1816 image = Cc["@mozilla.org/image/tools;1"] 1817 .getService(Ci.imgITools) 1818 .encodeImage(image, flavor); 1819 } 1820 1821 if (!(image instanceof Ci.nsIInputStream)) { 1822 throw new Error("Unable to read image data"); 1823 } 1824 1825 const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 1826 Ci.nsIBinaryInputStream 1827 ); 1828 binaryStream.setInputStream(image); 1829 const available = binaryStream.available(); 1830 const buffer = new ArrayBuffer(available); 1831 is( 1832 binaryStream.readArrayBuffer(available, buffer), 1833 available, 1834 "Read expected amount of data" 1835 ); 1836 1837 // We are going to load the image in the content page to measure its size. 1838 // We don't want to insert the image directly in the browser's document 1839 // (which is value of the global `document` here). Doing so might push the 1840 // toolbox upwards, shrink the content page and fail the fullpage screenshot 1841 // test. 1842 return SpecialPowers.spawn( 1843 gBrowser.selectedBrowser, 1844 [buffer], 1845 async function (_buffer) { 1846 const img = content.document.createElement("img"); 1847 const loaded = new Promise(r => { 1848 img.addEventListener("load", r, { once: true }); 1849 }); 1850 1851 // Build a URL from the buffer passed to the ContentTask 1852 const url = content.URL.createObjectURL( 1853 new Blob([_buffer], { type: "image/png" }) 1854 ); 1855 1856 // Load the image 1857 img.src = url; 1858 content.document.documentElement.appendChild(img); 1859 1860 info("Waiting for the clipboard image to load in the content page"); 1861 await loaded; 1862 1863 // Remove the image and revoke the URL. 1864 img.remove(); 1865 content.URL.revokeObjectURL(url); 1866 1867 return { 1868 width: img.width, 1869 height: img.height, 1870 }; 1871 } 1872 ); 1873 }