shared-head.js (109296B)
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 // This file is loaded in a `spawn` context sometimes which doesn't have, 6 // `Assert`, so we can't use its comparison functions. 7 /* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */ 8 9 /** 10 * Helper methods to drive with the debugger during mochitests. This file can be safely 11 * required from other panel test files. 12 */ 13 14 "use strict"; 15 16 /* eslint-disable no-unused-vars */ 17 18 // We can't use "import globals from head.js" because of bug 1395426. 19 // So workaround by manually importing the few symbols we are using from it. 20 // (Note that only ./mach eslint devtools/client fails while devtools/client/debugger passes) 21 /* global EXAMPLE_URL, ContentTask */ 22 23 // Assume that shared-head is always imported before this file 24 /* import-globals-from ../../../shared/test/shared-head.js */ 25 26 /** 27 * Helper method to create a "dbg" context for other tools to use 28 */ 29 function createDebuggerContext(toolbox) { 30 const panel = toolbox.getPanel("jsdebugger"); 31 const win = panel.panelWin; 32 33 return { 34 ...win.dbg, 35 commands: toolbox.commands, 36 toolbox, 37 win, 38 panel, 39 }; 40 } 41 42 var { Toolbox } = require("devtools/client/framework/toolbox"); 43 const asyncStorage = require("devtools/shared/async-storage"); 44 45 const { 46 getSelectedLocation, 47 } = require("devtools/client/debugger/src/utils/selected-location"); 48 const { 49 createLocation, 50 } = require("devtools/client/debugger/src/utils/location"); 51 52 const { 53 resetSchemaVersion, 54 } = require("devtools/client/debugger/src/utils/prefs"); 55 56 const { 57 getUnicodeUrlPath, 58 } = require("resource://devtools/client/shared/unicode-url.js"); 59 60 const { 61 isGeneratedId, 62 } = require("devtools/client/shared/source-map-loader/index"); 63 64 const DEBUGGER_L10N = new LocalizationHelper( 65 "devtools/client/locales/debugger.properties" 66 ); 67 68 /** 69 * Waits for `predicate()` to be true. `state` is the redux app state. 70 * 71 * @param {object} dbg 72 * @param {Function} predicate 73 * @param {string} msg 74 * @return {Promise} 75 */ 76 function waitForState(dbg, predicate, msg = "") { 77 return new Promise(resolve => { 78 info(`Waiting for state change: ${msg}`); 79 let result = predicate(dbg.store.getState()); 80 if (result) { 81 info( 82 `--> The state was immediately correct (should rather do an immediate assertion?)` 83 ); 84 resolve(result); 85 return; 86 } 87 88 const unsubscribe = dbg.store.subscribe( 89 () => { 90 result = predicate(dbg.store.getState()); 91 if (result) { 92 info(`Finished waiting for state change: ${msg}`); 93 unsubscribe(); 94 resolve(result); 95 } 96 }, 97 // The `visibilityHandlerStore` wrapper may prevent the test helper from being 98 // notified about store updates while the debugger is in background. 99 { ignoreVisibility: true } 100 ); 101 }); 102 } 103 104 /** 105 * Waits for sources to be loaded. 106 * 107 * @memberof mochitest/waits 108 * @param {object} dbg 109 * @param {Array} sources 110 * @return {Promise} 111 * @static 112 */ 113 async function waitForSources(dbg, ...sources) { 114 if (sources.length === 0) { 115 return; 116 } 117 118 info(`Waiting on sources: ${sources.join(", ")}`); 119 await Promise.all( 120 sources.map(url => { 121 if (!sourceExists(dbg, url)) { 122 return waitForState( 123 dbg, 124 () => sourceExists(dbg, url), 125 `source ${url} exists` 126 ); 127 } 128 return Promise.resolve(); 129 }) 130 ); 131 132 info(`Finished waiting on sources: ${sources.join(", ")}`); 133 } 134 135 /** 136 * Waits for a source to be loaded. 137 * 138 * @memberof mochitest/waits 139 * @param {object} dbg 140 * @param {string} source 141 * @return {Promise} 142 * @static 143 */ 144 function waitForSource(dbg, url) { 145 return waitForState( 146 dbg, 147 () => findSource(dbg, url, { silent: true }), 148 "source exists" 149 ); 150 } 151 152 async function waitForElement(dbg, name, ...args) { 153 info(`Waiting for debugger element by name: ${name}`); 154 await waitUntil(() => findElement(dbg, name, ...args)); 155 return findElement(dbg, name, ...args); 156 } 157 158 /** 159 * Wait for a count of given elements to be rendered on screen. 160 * 161 * @param {DebuggerPanel} dbg 162 * @param {string} name 163 * @param {Integer} count: Number of elements to match. Defaults to 1. 164 * @param {boolean} countStrictlyEqual: When set to true, will wait until the exact number 165 * of elements is displayed on screen. When undefined or false, will wait 166 * until there's at least `${count}` elements on screen (e.g. if count 167 * is 1, it will resolve if there are 2 elements rendered). 168 */ 169 async function waitForAllElements( 170 dbg, 171 name, 172 count = 1, 173 countStrictlyEqual = false 174 ) { 175 info(`Waiting for N=${count} debugger elements by name: ${name}`); 176 await waitUntil(() => { 177 const elsCount = findAllElements(dbg, name).length; 178 return countStrictlyEqual ? elsCount === count : elsCount >= count; 179 }); 180 return findAllElements(dbg, name); 181 } 182 183 async function waitForElementWithSelector(dbg, selector) { 184 info(`Waiting for debugger element by selector: ${selector}`); 185 await waitUntil(() => findElementWithSelector(dbg, selector)); 186 return findElementWithSelector(dbg, selector); 187 } 188 189 function waitForRequestsToSettle(dbg) { 190 return dbg.commands.client.waitForRequestsToSettle(); 191 } 192 193 function assertClass(el, className, exists = true) { 194 if (exists) { 195 ok(el.classList.contains(className), `${className} class exists`); 196 } else { 197 ok(!el.classList.contains(className), `${className} class does not exist`); 198 } 199 } 200 201 async function waitForSelectedLocation(dbg, line, column) { 202 // Assert the state in Redux 203 await waitForState(dbg, () => { 204 const location = dbg.selectors.getSelectedLocation(); 205 return ( 206 location && 207 location.line == line && 208 // location's column is 0-based, while all line and columns mentioned in tests 209 // are 1-based. 210 (typeof column == "number" ? location.column + 1 == column : true) 211 ); 212 }); 213 214 // Also assert the cursor position in CodeMirror 215 await waitFor(function () { 216 const cursor = getCMEditor(dbg).getSelectionCursor(); 217 if (!cursor) { 218 return false; 219 } 220 if (line && cursor.from.line != line) { 221 return false; 222 } 223 // Asserted column is 1-based while CodeMirror's cursor column is 0-based 224 if (column && cursor.from.ch + 1 != column) { 225 return false; 226 } 227 return true; 228 }); 229 } 230 231 /** 232 * Wait for a given source to be selected and ready. 233 * 234 * @memberof mochitest/waits 235 * @param {object} dbg 236 * @param {null|string|Source} sourceOrUrl Optional. Either a source URL (string) or a source object (typically fetched via `findSource`) 237 * @return {Promise} 238 * @static 239 */ 240 function waitForSelectedSource(dbg, sourceOrUrl) { 241 const { 242 getSelectedSourceTextContent, 243 getBreakableLines, 244 getSourceActorsForSource, 245 getSourceActorBreakableLines, 246 getFirstSourceActorForGeneratedSource, 247 getSelectedFrame, 248 getCurrentThread, 249 } = dbg.selectors; 250 251 return waitForState( 252 dbg, 253 () => { 254 const location = dbg.selectors.getSelectedLocation() || {}; 255 const sourceTextContent = getSelectedSourceTextContent(); 256 if (!sourceTextContent) { 257 return false; 258 } 259 260 if (sourceOrUrl) { 261 // Second argument is either a source URL (string) 262 // or a Source object. 263 if (typeof sourceOrUrl == "string") { 264 const url = location.source.url; 265 if ( 266 typeof url != "string" || 267 (!url.includes(encodeURI(sourceOrUrl)) && 268 !url.includes(sourceOrUrl)) 269 ) { 270 return false; 271 } 272 } else if (location.source.id != sourceOrUrl.id) { 273 return false; 274 } 275 } 276 277 // Finaly wait for breakable lines to be set 278 if (location.source.isHTML) { 279 // For HTML sources we need to wait for each source actor to be processed. 280 // getBreakableLines will return the aggregation without being able to know 281 // if that's complete, with all the source actors. 282 const sourceActors = getSourceActorsForSource(location.source.id); 283 const allSourceActorsProcessed = sourceActors.every( 284 sourceActor => !!getSourceActorBreakableLines(sourceActor.id) 285 ); 286 return allSourceActorsProcessed; 287 } 288 289 if (!getBreakableLines(location.source.id)) { 290 return false; 291 } 292 293 // Also ensure that CodeMirror updated its content 294 return getEditorContent(dbg) !== DEBUGGER_L10N.getStr("loadingText"); 295 }, 296 "selected source" 297 ); 298 } 299 300 /** 301 * The generated source of WASM source are WASM binary file, 302 * which have many broken/disabled features in the debugger. 303 * 304 * They especially have a very special behavior in CodeMirror 305 * where line labels aren't line number, but hex addresses. 306 */ 307 function isWasmBinarySource(source) { 308 return source.isWasm && !source.isOriginal; 309 } 310 311 function getVisibleSelectedFrameLine(dbg) { 312 const frame = dbg.selectors.getVisibleSelectedFrame(); 313 return frame?.location.line; 314 } 315 316 function getVisibleSelectedFrameColumn(dbg) { 317 const frame = dbg.selectors.getVisibleSelectedFrame(); 318 return frame?.location.column; 319 } 320 321 /** 322 * Assert that a given line is breakable or not. 323 * Verify that CodeMirror gutter is grayed out via the empty line classname if not breakable. 324 */ 325 async function assertLineIsBreakable(dbg, file, line, shouldBeBreakable) { 326 const el = await getNodeAtEditorGutterLine(dbg, line); 327 const lineText = `${line}| ${el.innerText.substring(0, 50)}${ 328 el.innerText.length > 50 ? "…" : "" 329 } — in ${file}`; 330 // When a line is not breakable, the "empty-line" class is added 331 // and the line is greyed out 332 if (shouldBeBreakable) { 333 ok(!el.classList.contains("empty-line"), `${lineText} should be breakable`); 334 } else { 335 ok( 336 el.classList.contains("empty-line"), 337 `${lineText} should NOT be breakable` 338 ); 339 } 340 } 341 342 /** 343 * Assert that the debugger is highlighting the correct location. 344 * 345 * @memberof mochitest/asserts 346 * @param {object} dbg 347 * @param {string} source 348 * @param {number} line 349 * @static 350 */ 351 function assertHighlightLocation(dbg, source, line) { 352 source = findSource(dbg, source); 353 354 // Check the selected source 355 is( 356 dbg.selectors.getSelectedSource().url, 357 source.url, 358 "source url is correct" 359 ); 360 361 // Check the highlight line 362 const lineEl = findElement(dbg, "highlightLine"); 363 ok(lineEl, "Line is highlighted"); 364 365 is( 366 findAllElements(dbg, "highlightLine").length, 367 1, 368 "Only 1 line is highlighted" 369 ); 370 371 ok(isVisibleInEditor(dbg, lineEl), "Highlighted line is visible"); 372 373 const lineInfo = getCMEditor(dbg).lineInfo(line); 374 ok(lineInfo.wrapClass.includes("highlight-line"), "Line is highlighted"); 375 } 376 377 /** 378 * Helper function for assertPausedAtSourceAndLine. 379 * 380 * Assert that CodeMirror reports to be paused at the given line/column. 381 */ 382 async function _assertDebugLine(dbg, line, column) { 383 const source = dbg.selectors.getSelectedSource(); 384 // WASM lines are hex addresses which have to be mapped to decimal line number 385 if (isWasmBinarySource(source)) { 386 line = wasmOffsetToLine(dbg, line); 387 } 388 389 // Check the debug line 390 // cm6 lines are 1-based, while cm5 are 0-based, to keep compatibility with 391 // .lineInfo usage in other locations. 392 const lineInfo = getCMEditor(dbg).lineInfo(line); 393 const sourceTextContent = dbg.selectors.getSelectedSourceTextContent(); 394 if (source && !sourceTextContent) { 395 const url = source.url; 396 ok( 397 false, 398 `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.` 399 ); 400 return; 401 } 402 403 // Scroll the line into view to make sure the content 404 // on the line is rendered and in the dom. 405 await scrollEditorIntoView(dbg, line, 0); 406 407 if (!lineInfo.wrapClass) { 408 const pauseLine = getVisibleSelectedFrameLine(dbg); 409 ok(false, `Expected pause line on line ${line}, it is on ${pauseLine}`); 410 return; 411 } 412 413 // Consider pausing on error also as being paused 414 ok( 415 lineInfo?.wrapClass.includes("paused-line") || 416 lineInfo?.wrapClass.includes("new-debug-line-error"), 417 `Line ${line} is not highlighted as paused` 418 ); 419 420 const pausedLine = 421 findElement(dbg, "pausedLine") || findElement(dbg, "debugErrorLine"); 422 423 is( 424 findAllElements(dbg, "pausedLine").length + 425 findAllElements(dbg, "debugErrorLine").length, 426 1, 427 "There is only one line" 428 ); 429 430 ok(isVisibleInEditor(dbg, pausedLine), "debug line is visible"); 431 432 const editorLineEl = getCMEditor(dbg).getElementAtLine(line); 433 const pauseLocationMarker = editorLineEl.querySelector(".paused-location"); 434 is( 435 pauseLocationMarker.cmView.widget.line, 436 line, 437 "The paused caret is at the right line" 438 ); 439 is( 440 pauseLocationMarker.cmView.widget.column, 441 column, 442 "The paused caret is at the right column" 443 ); 444 info(`Paused on line ${line}`); 445 } 446 447 /** 448 * Make sure the debugger is paused at a certain source ID and line. 449 * 450 * @param {object} dbg 451 * @param {string} expectedSourceId 452 * @param {number} expectedLine 453 * @param {number} [expectedColumn] 454 */ 455 async function assertPausedAtSourceAndLine( 456 dbg, 457 expectedSourceId, 458 expectedLine, 459 expectedColumn 460 ) { 461 // Check that the debugger is paused. 462 assertPaused(dbg); 463 464 // Check that the paused location is correctly rendered. 465 ok(isSelectedFrameSelected(dbg), "top frame's source is selected"); 466 467 // Check the pause location 468 const pauseLine = getVisibleSelectedFrameLine(dbg); 469 is( 470 pauseLine, 471 expectedLine, 472 "Redux state for currently selected frame's line is correct" 473 ); 474 475 const selectedSource = dbg.selectors.getSelectedSource(); 476 // WASM binary source is pausing at 0 column, whereas visible selected frame returns 1 477 const pauseColumn = isWasmBinarySource(selectedSource) 478 ? 0 479 : getVisibleSelectedFrameColumn(dbg); 480 if (expectedColumn) { 481 // `pauseColumn` is 0-based, coming from internal state, 482 // while `expectedColumn` is manually passed from test scripts and so is 1-based. 483 is( 484 pauseColumn + 1, 485 expectedColumn, 486 "Redux state for currently selected frame's column is correct" 487 ); 488 } 489 await _assertDebugLine(dbg, pauseLine, pauseColumn); 490 491 ok(isVisibleInEditor(dbg, findElement(dbg, "gutters")), "gutter is visible"); 492 493 const frames = dbg.selectors.getCurrentThreadFrames(); 494 495 // WASM support is limited when we are on the generated binary source 496 if (isWasmBinarySource(selectedSource)) { 497 return; 498 } 499 500 ok(frames.length >= 1, "Got at least one frame"); 501 502 // Lets make sure we can assert both original and generated file locations when needed 503 const { source, line, column } = isGeneratedId(expectedSourceId) 504 ? frames[0].generatedLocation 505 : frames[0].location; 506 is(source.id, expectedSourceId, "Frame has correct source"); 507 is( 508 line, 509 expectedLine, 510 `Frame paused at line ${line}, but expected line ${expectedLine}` 511 ); 512 513 if (expectedColumn) { 514 // `column` is 0-based, coming from internal state, 515 // while `expectedColumn` is manually passed from test scripts and so is 1-based. 516 is( 517 column + 1, 518 expectedColumn, 519 `Frame paused at column ${ 520 column + 1 521 }, but expected column ${expectedColumn}` 522 ); 523 } 524 } 525 526 async function waitForThreadCount(dbg, count) { 527 return waitForState( 528 dbg, 529 state => dbg.selectors.getThreads(state).length == count 530 ); 531 } 532 533 async function waitForLoadedScopes(dbg) { 534 const scopes = await waitForElement(dbg, "scopes"); 535 // Since scopes auto-expand, we can assume they are loaded when there is a tree node 536 // with the aria-level attribute equal to "2". 537 info("Wait for loaded scopes - ie when a tree node has aria-level=2"); 538 await waitUntil(() => scopes.querySelector('.tree-node[aria-level="2"]')); 539 } 540 541 function waitForBreakpointCount(dbg, count) { 542 return waitForState(dbg, () => dbg.selectors.getBreakpointCount() == count); 543 } 544 545 function waitForBreakpoint(dbg, url, line) { 546 return waitForState(dbg, () => findBreakpoint(dbg, url, line)); 547 } 548 549 function waitForBreakpointRemoved(dbg, url, line) { 550 return waitForState(dbg, () => !findBreakpoint(dbg, url, line)); 551 } 552 553 /** 554 * Returns boolean for whether the debugger is paused. 555 * 556 * @param {object} dbg 557 */ 558 function isPaused(dbg) { 559 return dbg.selectors.getIsCurrentThreadPaused(); 560 } 561 562 /** 563 * Assert that the debugger is not currently paused. 564 * 565 * @param {object} dbg 566 * @param {string} msg 567 * Optional assertion message 568 */ 569 function assertNotPaused(dbg, msg = "client is not paused") { 570 ok(!isPaused(dbg), msg); 571 } 572 573 /** 574 * Assert that the debugger is currently paused. 575 * 576 * @param {object} dbg 577 */ 578 function assertPaused(dbg, msg = "client is paused") { 579 ok(isPaused(dbg), msg); 580 } 581 582 /** 583 * Waits for the debugger to be fully paused. 584 * 585 * @param {object} dbg 586 * @param {string} url 587 * Optional URL of the script we should be pausing on. 588 * @param {object} options 589 * {Boolean} shouldWaitForLoadScopes 590 * When paused in original files with original variable mapping disabled, scopes are 591 * not going to exist, lets not wait for it. defaults to true 592 */ 593 async function waitForPaused( 594 dbg, 595 url, 596 options = { 597 shouldWaitForLoadedScopes: true, 598 shouldWaitForInlinePreviews: true, 599 } 600 ) { 601 info("Waiting for the debugger to pause"); 602 const { getSelectedScope, getCurrentThread, getCurrentThreadFrames } = 603 dbg.selectors; 604 605 await waitForState( 606 dbg, 607 () => isPaused(dbg) && !!getSelectedScope(), 608 "paused" 609 ); 610 611 await waitForState(dbg, getCurrentThreadFrames, "fetched frames"); 612 613 if (options.shouldWaitForLoadedScopes) { 614 await waitForLoadedScopes(dbg); 615 } 616 617 await waitForSelectedSource(dbg, url); 618 619 if (options.shouldWaitForInlinePreviews) { 620 await waitForInlinePreviews(dbg); 621 } 622 } 623 624 /** 625 * Waits for the debugger to resume. 626 * 627 * @param {Objeect} dbg 628 */ 629 function waitForResumed(dbg) { 630 info("Waiting for the debugger to resume"); 631 return waitForState(dbg, () => !dbg.selectors.getIsCurrentThreadPaused()); 632 } 633 634 function waitForInlinePreviews(dbg) { 635 return waitForState(dbg, () => dbg.selectors.getInlinePreviews()); 636 } 637 638 function waitForCondition(dbg, condition) { 639 return waitForState(dbg, () => 640 dbg.selectors 641 .getBreakpointsList() 642 .find(bp => bp.options.condition == condition) 643 ); 644 } 645 646 function waitForLog(dbg, logValue) { 647 return waitForState(dbg, () => 648 dbg.selectors 649 .getBreakpointsList() 650 .find(bp => bp.options.logValue == logValue) 651 ); 652 } 653 654 async function waitForPausedThread(dbg, thread) { 655 return waitForState(dbg, () => dbg.selectors.getIsPaused(thread)); 656 } 657 658 function isSelectedFrameSelected(dbg) { 659 const frame = dbg.selectors.getVisibleSelectedFrame(); 660 661 // Make sure the source text is completely loaded for the 662 // source we are paused in. 663 const source = dbg.selectors.getSelectedSource(); 664 const sourceTextContent = dbg.selectors.getSelectedSourceTextContent(); 665 666 if (!source || !sourceTextContent) { 667 return false; 668 } 669 670 return source.id == frame.location.source.id; 671 } 672 673 /** 674 * Checks to see if the frame is selected and the displayed title is correct. 675 * 676 * @param {object} dbg 677 * @param {DOM Node} frameElement 678 * @param {string} expectedTitle 679 */ 680 function assertFrameIsSelected(dbg, frameElement, expectedTitle) { 681 const selectedFrame = dbg.selectors.getSelectedFrame(); 682 ok(frameElement.classList.contains("selected"), "The frame is selected"); 683 is( 684 frameElement.querySelector(".title").innerText, 685 expectedTitle, 686 "The selected frame element has the expected title" 687 ); 688 // For `<anonymous>` frames, there is likely no displayName 689 is( 690 selectedFrame.displayName, 691 expectedTitle == "<anonymous>" ? undefined : expectedTitle, 692 "The selected frame has the correct display title" 693 ); 694 } 695 696 /** 697 * Checks to see if the frame is not selected. 698 * 699 * @param {object} dbg 700 * @param {DOM Node} frameElement 701 * @param {string} expectedTitle 702 */ 703 function assertFrameIsNotSelected(dbg, frameElement, expectedTitle) { 704 const selectedFrame = dbg.selectors.getSelectedFrame(); 705 ok(!frameElement.classList.contains("selected"), "The frame is selected"); 706 is( 707 frameElement.querySelector(".title").innerText, 708 expectedTitle, 709 "The selected frame element has the expected title" 710 ); 711 } 712 713 /** 714 * Clear all the debugger related preferences. 715 */ 716 async function clearDebuggerPreferences(prefs = []) { 717 resetSchemaVersion(); 718 await asyncStorage.clear(); 719 Services.prefs.clearUserPref("devtools.debugger.alphabetize-outline"); 720 Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions"); 721 Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions"); 722 Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions"); 723 Services.prefs.clearUserPref("devtools.debugger.pending-selected-location"); 724 Services.prefs.clearUserPref("devtools.debugger.expressions"); 725 Services.prefs.clearUserPref("devtools.debugger.breakpoints-visible"); 726 Services.prefs.clearUserPref("devtools.debugger.call-stack-visible"); 727 Services.prefs.clearUserPref("devtools.debugger.scopes-visible"); 728 Services.prefs.clearUserPref("devtools.debugger.skip-pausing"); 729 730 for (const pref of prefs) { 731 await pushPref(...pref); 732 } 733 } 734 735 /** 736 * Intilializes the debugger. 737 * 738 * @memberof mochitest 739 * @param {string} url 740 * @return {Promise} dbg 741 * @static 742 */ 743 744 async function initDebugger(url, ...sources) { 745 // We depend on EXAMPLE_URLs origin to do cross origin/process iframes via 746 // EXAMPLE_REMOTE_URL. If the top level document origin changes, 747 // we may break this. So be careful if you want to change EXAMPLE_URL. 748 return initDebuggerWithAbsoluteURL(EXAMPLE_URL + url, ...sources); 749 } 750 751 async function initDebuggerWithAbsoluteURL(url, ...sources) { 752 await clearDebuggerPreferences(); 753 const toolbox = await openNewTabAndToolbox(url, "jsdebugger"); 754 const dbg = createDebuggerContext(toolbox); 755 756 await waitForSources(dbg, ...sources); 757 return dbg; 758 } 759 760 async function initPane(url, pane, prefs) { 761 await clearDebuggerPreferences(prefs); 762 return openNewTabAndToolbox(EXAMPLE_URL + url, pane); 763 } 764 765 /** 766 * Returns a source that matches a given filename, or a URL. 767 * This also accept a source as input argument, in such case it just returns it. 768 * 769 * @param {object} dbg 770 * @param {string} filenameOrUrlOrSource 771 * The typical case will be to pass only a filename, 772 * but you may also pass a full URL to match sources without filesnames like data: URL 773 * or pass the source itself, which is just returned. 774 * @param {object} options 775 * @param {boolean} options.silent 776 * If true, won't throw if the source is missing. 777 * @return {object} source 778 */ 779 function findSource( 780 dbg, 781 filenameOrUrlOrSource, 782 { silent } = { silent: false } 783 ) { 784 if (typeof filenameOrUrlOrSource !== "string") { 785 // Support passing in a source object itself all APIs that use this 786 // function support both styles 787 return filenameOrUrlOrSource; 788 } 789 790 const sources = dbg.selectors.getSourceList(); 791 const source = sources.find(s => { 792 // Sources don't have a file name attribute, we need to compute it here: 793 const sourceFileName = s.url 794 ? getUnicodeUrlPath(s.url.substring(s.url.lastIndexOf("/") + 1)) 795 : ""; 796 797 // The input argument may either be only the filename, or the complete URL 798 // This helps match sources whose URL doesn't contain a filename, like data: URLs 799 return ( 800 sourceFileName == filenameOrUrlOrSource || s.url == filenameOrUrlOrSource 801 ); 802 }); 803 804 if (!source) { 805 if (silent) { 806 return false; 807 } 808 809 throw new Error(`Unable to find source: ${filenameOrUrlOrSource}`); 810 } 811 812 return source; 813 } 814 815 function findSourceContent(dbg, url, opts) { 816 const source = findSource(dbg, url, opts); 817 818 if (!source) { 819 return null; 820 } 821 const content = dbg.selectors.getSettledSourceTextContent( 822 createLocation({ 823 source, 824 }) 825 ); 826 827 if (!content) { 828 return null; 829 } 830 831 if (content.state !== "fulfilled") { 832 throw new Error(`Expected loaded source, got${content.value}`); 833 } 834 835 return content.value; 836 } 837 838 function sourceExists(dbg, url) { 839 return !!findSource(dbg, url, { silent: true }); 840 } 841 842 function waitForLoadedSource(dbg, url) { 843 return waitForState( 844 dbg, 845 () => { 846 const source = findSource(dbg, url, { silent: true }); 847 return ( 848 source && 849 dbg.selectors.getSettledSourceTextContent( 850 createLocation({ 851 source, 852 }) 853 ) 854 ); 855 }, 856 "loaded source" 857 ); 858 } 859 860 /** 861 * Selects the source node for a specific source 862 * from the source tree. 863 * 864 * @param {object} dbg 865 * @param {string} filename - The filename for the specific source 866 */ 867 async function selectSourceFromSourceTree(dbg, fileName) { 868 info(`Selecting '${fileName}' source from source tree`); 869 // Ensure that the source is visible in the tree before trying to click on it 870 const elt = await waitForSourceInSourceTree(dbg, fileName); 871 elt.scrollIntoView(); 872 clickDOMElement(dbg, elt); 873 await waitForSelectedSource(dbg, fileName); 874 await waitFor( 875 () => getEditorContent(dbg) !== `Loading…`, 876 "Wait for source to completely load" 877 ); 878 } 879 880 /** 881 * Similar to selectSourceFromSourceTree, but with a precise location 882 * in the source tree. 883 */ 884 async function selectSourceFromSourceTreeWithIndex( 885 dbg, 886 fileName, 887 sourcePosition, 888 message 889 ) { 890 info(message); 891 await clickElement(dbg, "sourceNode", sourcePosition); 892 await waitForSelectedSource(dbg, fileName); 893 await waitFor( 894 () => getEditorContent(dbg) !== `Loading…`, 895 "Wait for source to completely load" 896 ); 897 } 898 899 /** 900 * Trigger a context menu in the debugger source tree 901 * 902 * @param {object} dbg 903 * @param {Obejct} sourceTreeNode - The node in the source tree which the context menu 904 * item needs to be triggered on. 905 * @param {string} contextMenuItem - The id for the context menu item to be selected 906 */ 907 async function triggerSourceTreeContextMenu( 908 dbg, 909 sourceTreeNode, 910 contextMenuItem 911 ) { 912 const onContextMenu = waitForContextMenu(dbg); 913 rightClickEl(dbg, sourceTreeNode); 914 const menupopup = await onContextMenu; 915 const onHidden = new Promise(resolve => { 916 menupopup.addEventListener("popuphidden", resolve, { once: true }); 917 }); 918 selectDebuggerContextMenuItem(dbg, contextMenuItem); 919 await onHidden; 920 } 921 922 /** 923 * Selects the source. 924 * 925 * @memberof mochitest/actions 926 * @param {object} dbg 927 * @param {string} url 928 * @param {number} line 929 * @param {number} column 930 * @return {Promise} 931 * @static 932 */ 933 async function selectSource(dbg, url, line, column) { 934 const source = findSource(dbg, url); 935 936 await dbg.actions.selectLocation(createLocation({ source, line, column }), { 937 keepContext: false, 938 }); 939 return waitForSelectedSource(dbg, source); 940 } 941 942 async function closeTab(dbg, url) { 943 const source = findSource(dbg, url); 944 await dbg.actions.closeTabForSource(source); 945 } 946 947 function countTabs(dbg) { 948 // The sourceTabs elements won't be rendered if there is no source. 949 const sourceTabs = findElement(dbg, "sourceTabs"); 950 return sourceTabs ? sourceTabs.children.length : 0; 951 } 952 953 /** 954 * Steps over. 955 * 956 * @memberof mochitest/actions 957 * @param {object} dbg 958 * @param {object} pauseOptions 959 * @return {Promise} 960 * @static 961 */ 962 async function stepOver(dbg, pauseOptions) { 963 const pauseLine = getVisibleSelectedFrameLine(dbg); 964 info(`Stepping over from ${pauseLine}`); 965 await dbg.actions.stepOver(); 966 return waitForPaused(dbg, null, pauseOptions); 967 } 968 969 /** 970 * Steps in. 971 * 972 * @memberof mochitest/actions 973 * @param {object} dbg 974 * @return {Promise} 975 * @static 976 */ 977 async function stepIn(dbg) { 978 const pauseLine = getVisibleSelectedFrameLine(dbg); 979 info(`Stepping in from ${pauseLine}`); 980 await dbg.actions.stepIn(); 981 return waitForPaused(dbg); 982 } 983 984 /** 985 * Steps out. 986 * 987 * @memberof mochitest/actions 988 * @param {object} dbg 989 * @return {Promise} 990 * @static 991 */ 992 async function stepOut(dbg) { 993 const pauseLine = getVisibleSelectedFrameLine(dbg); 994 info(`Stepping out from ${pauseLine}`); 995 await dbg.actions.stepOut(); 996 return waitForPaused(dbg); 997 } 998 999 /** 1000 * Resumes. 1001 * 1002 * @memberof mochitest/actions 1003 * @param {object} dbg 1004 * @return {Promise} 1005 * @static 1006 */ 1007 async function resume(dbg) { 1008 const pauseLine = getVisibleSelectedFrameLine(dbg); 1009 info(`Resuming from ${pauseLine}`); 1010 const onResumed = waitForResumed(dbg); 1011 await dbg.actions.resume(); 1012 return onResumed; 1013 } 1014 1015 function deleteExpression(dbg, input) { 1016 info(`Delete expression "${input}"`); 1017 return dbg.actions.deleteExpression({ input }); 1018 } 1019 1020 /** 1021 * Reloads the debuggee. 1022 * 1023 * @memberof mochitest/actions 1024 * @param {object} dbg 1025 * @param {Array} sources 1026 * @return {Promise} 1027 * @static 1028 */ 1029 async function reload(dbg, ...sources) { 1030 await reloadBrowser(); 1031 return waitForSources(dbg, ...sources); 1032 } 1033 1034 // Only use this method when the page is paused by the debugger 1035 // during page load and we navigate away without resuming. 1036 // 1037 // In this particular scenario, the page will never be "loaded". 1038 // i.e. emit DOCUMENT_EVENT's dom-complete 1039 // And consequently, debugger panel won't emit "reloaded" event. 1040 async function reloadWhenPausedBeforePageLoaded(dbg, ...sources) { 1041 // But we can at least listen for the next DOCUMENT_EVENT's dom-loading, 1042 // which should be fired even if the page is pause the earliest. 1043 const { resourceCommand } = dbg.commands; 1044 const { onResource: onTopLevelDomLoading } = 1045 await resourceCommand.waitForNextResource( 1046 resourceCommand.TYPES.DOCUMENT_EVENT, 1047 { 1048 ignoreExistingResources: true, 1049 predicate: resource => 1050 resource.targetFront.isTopLevel && resource.name === "dom-loading", 1051 } 1052 ); 1053 1054 gBrowser.reloadTab(gBrowser.selectedTab); 1055 1056 info("Wait for DOCUMENT_EVENT dom-loading after reload"); 1057 await onTopLevelDomLoading; 1058 return waitForSources(dbg, ...sources); 1059 } 1060 1061 /** 1062 * Navigates the debuggee to another url. 1063 * 1064 * @memberof mochitest/actions 1065 * @param {object} dbg 1066 * @param {string} url 1067 * @param {Array} sources 1068 * @return {Promise} 1069 * @static 1070 */ 1071 async function navigate(dbg, url, ...sources) { 1072 return navigateToAbsoluteURL(dbg, EXAMPLE_URL + url, ...sources); 1073 } 1074 1075 /** 1076 * Navigates the debuggee to another absolute url. 1077 * 1078 * @memberof mochitest/actions 1079 * @param {object} dbg 1080 * @param {string} url 1081 * @param {Array} sources 1082 * @return {Promise} 1083 * @static 1084 */ 1085 async function navigateToAbsoluteURL(dbg, url, ...sources) { 1086 await navigateTo(url); 1087 return waitForSources(dbg, ...sources); 1088 } 1089 1090 function getFirstBreakpointColumn(dbg, source, line) { 1091 const position = dbg.selectors.getFirstBreakpointPosition( 1092 createLocation({ 1093 line, 1094 source, 1095 }) 1096 ); 1097 1098 return getSelectedLocation(position, source).column; 1099 } 1100 1101 function isMatchingLocation(location1, location2) { 1102 return ( 1103 location1?.source.id == location2?.source.id && 1104 location1?.line == location2?.line && 1105 location1?.column == location2?.column 1106 ); 1107 } 1108 1109 function getBreakpointForLocation(dbg, location) { 1110 if (!location) { 1111 return undefined; 1112 } 1113 1114 const isGeneratedSource = isGeneratedId(location.source.id); 1115 return dbg.selectors.getBreakpointsList().find(bp => { 1116 const loc = isGeneratedSource ? bp.generatedLocation : bp.location; 1117 return isMatchingLocation(loc, location); 1118 }); 1119 } 1120 1121 /** 1122 * Adds a breakpoint to a source at line/col. 1123 * 1124 * @memberof mochitest/actions 1125 * @param {object} dbg 1126 * @param {string} source 1127 * @param {number} line 1128 * @param {number} col 1129 * @return {Promise} 1130 * @static 1131 */ 1132 async function addBreakpoint(dbg, source, line, column, options) { 1133 source = findSource(dbg, source); 1134 const bpCount = dbg.selectors.getBreakpointCount(); 1135 const onBreakpoint = waitForDispatch(dbg.store, "SET_BREAKPOINT"); 1136 await dbg.actions.addBreakpoint( 1137 // column is 0-based internally, but tests are using 1-based. 1138 createLocation({ source, line, column: column - 1 }), 1139 options 1140 ); 1141 await onBreakpoint; 1142 is( 1143 dbg.selectors.getBreakpointCount(), 1144 bpCount + 1, 1145 "a new breakpoint was created" 1146 ); 1147 } 1148 1149 // use shortcut to open conditional panel. 1150 function setConditionalBreakpointWithKeyboardShortcut(dbg, condition) { 1151 pressKey(dbg, "toggleCondPanel"); 1152 return typeInPanel(dbg, condition); 1153 } 1154 1155 /** 1156 * Similar to `addBreakpoint`, but uses the UI instead or calling 1157 * the actions directly. This only support breakpoint on lines, 1158 * not on a specific column. 1159 */ 1160 async function addBreakpointViaGutter(dbg, line) { 1161 info(`Add breakpoint via the editor on line ${line}`); 1162 await clickGutter(dbg, line); 1163 return waitForDispatch(dbg.store, "SET_BREAKPOINT"); 1164 } 1165 1166 async function removeBreakpointViaGutter(dbg, line) { 1167 const onRemoved = waitForDispatch(dbg.store, "REMOVE_BREAKPOINT"); 1168 await clickGutter(dbg, line); 1169 await onRemoved; 1170 } 1171 1172 function disableBreakpoint(dbg, source, line, column) { 1173 if (column === 0) { 1174 throw new Error("disableBreakpoint expect a 1-based column argument"); 1175 } 1176 // `internalColumn` is 0-based internally, while `column` manually defined in test scripts is 1-based. 1177 const internalColumn = column 1178 ? column - 1 1179 : getFirstBreakpointColumn(dbg, source, line); 1180 const location = createLocation({ 1181 source, 1182 line, 1183 column: internalColumn, 1184 }); 1185 const bp = getBreakpointForLocation(dbg, location); 1186 return dbg.actions.disableBreakpoint(bp); 1187 } 1188 1189 function findBreakpoint(dbg, url, line) { 1190 const source = findSource(dbg, url); 1191 return dbg.selectors.getBreakpointsForSource(source, line)[0]; 1192 } 1193 1194 // helper for finding column breakpoints. 1195 function findColumnBreakpoint(dbg, url, line, column) { 1196 const source = findSource(dbg, url); 1197 const lineBreakpoints = dbg.selectors.getBreakpointsForSource(source, line); 1198 1199 return lineBreakpoints.find(bp => { 1200 return source.isOriginal 1201 ? bp.location.column === column 1202 : bp.generatedLocation.column === column; 1203 }); 1204 } 1205 1206 async function loadAndAddBreakpoint(dbg, filename, line, column) { 1207 const { 1208 selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap }, 1209 } = dbg; 1210 1211 await waitForSources(dbg, filename); 1212 1213 ok(true, "Original sources exist"); 1214 const source = findSource(dbg, filename); 1215 1216 await selectSource(dbg, source); 1217 1218 // Test that breakpoint is not off by a line. 1219 await addBreakpoint(dbg, source, line, column); 1220 1221 is(getBreakpointCount(), 1, "One breakpoint exists"); 1222 // column is 0-based internally, but tests are using 1-based. 1223 if (!getBreakpoint(createLocation({ source, line, column: column - 1 }))) { 1224 const breakpoints = getBreakpointsMap(); 1225 const id = Object.keys(breakpoints).pop(); 1226 const loc = breakpoints[id].location; 1227 ok( 1228 false, 1229 `Breakpoint has correct line ${line}, column ${column}, but was line ${ 1230 loc.line 1231 } column ${loc.column + 1}` 1232 ); 1233 } 1234 1235 return source; 1236 } 1237 1238 async function invokeWithBreakpoint( 1239 dbg, 1240 fnName, 1241 filename, 1242 { line, column }, 1243 handler, 1244 pauseOptions 1245 ) { 1246 const source = await loadAndAddBreakpoint(dbg, filename, line, column); 1247 1248 const invokeResult = invokeInTab(fnName); 1249 1250 const invokeFailed = await Promise.race([ 1251 waitForPaused(dbg, null, pauseOptions), 1252 invokeResult.then( 1253 () => new Promise(() => {}), 1254 () => true 1255 ), 1256 ]); 1257 1258 if (invokeFailed) { 1259 await invokeResult; 1260 return; 1261 } 1262 1263 await assertPausedAtSourceAndLine( 1264 dbg, 1265 findSource(dbg, filename).id, 1266 line, 1267 column 1268 ); 1269 1270 await removeBreakpoint(dbg, source.id, line, column); 1271 1272 is(dbg.selectors.getBreakpointCount(), 0, "Breakpoint reverted"); 1273 1274 await handler(source); 1275 1276 await resume(dbg); 1277 1278 // eslint-disable-next-line max-len 1279 // If the invoke errored later somehow, capture here so the error is reported nicely. 1280 await invokeResult; 1281 } 1282 1283 async function togglePrettyPrint(dbg) { 1284 const source = dbg.selectors.getSelectedSource(); 1285 clickElement(dbg, "prettyPrintButton"); 1286 if (source.isPrettyPrinted) { 1287 await waitForSelectedSource(dbg, source.generatedSource); 1288 } else { 1289 const prettyURL = source.url ? source.url : source.id.split("/").at(-1); 1290 await waitForSelectedSource(dbg, prettyURL + ":formatted"); 1291 } 1292 } 1293 1294 async function expandAllScopes(dbg) { 1295 const scopes = await waitForElement(dbg, "scopes"); 1296 const scopeElements = scopes.querySelectorAll( 1297 '.tree-node[aria-level="1"][data-expandable="true"]:not([aria-expanded="true"])' 1298 ); 1299 const indices = Array.from(scopeElements, el => { 1300 return Array.prototype.indexOf.call(el.parentNode.childNodes, el); 1301 }).reverse(); 1302 1303 for (const index of indices) { 1304 await toggleScopeNode(dbg, index + 1); 1305 } 1306 } 1307 1308 async function assertScopes(dbg, items) { 1309 await expandAllScopes(dbg); 1310 1311 for (const [i, val] of items.entries()) { 1312 if (Array.isArray(val)) { 1313 is(getScopeNodeLabel(dbg, i + 1), val[0]); 1314 is( 1315 getScopeNodeValue(dbg, i + 1), 1316 val[1], 1317 `"${val[0]}" has the expected "${val[1]}" value` 1318 ); 1319 } else { 1320 is(getScopeNodeLabel(dbg, i + 1), val); 1321 } 1322 } 1323 1324 is(getScopeNodeLabel(dbg, items.length + 1), "Window"); 1325 } 1326 1327 function findSourceTreeThreadByName(dbg, name) { 1328 return [...findAllElements(dbg, "sourceTreeThreads")].find(el => { 1329 return el.textContent.includes(name); 1330 }); 1331 } 1332 1333 function findSourceTreeGroupByName(dbg, name) { 1334 return [...findAllElements(dbg, "sourceTreeGroups")].find(el => { 1335 return el.textContent.includes(name); 1336 }); 1337 } 1338 1339 function findSourceNodeWithText(dbg, text) { 1340 return [...findAllElements(dbg, "sourceNodes")].find(el => { 1341 return el.textContent.includes(text); 1342 }); 1343 } 1344 1345 /** 1346 * Assert the icon type used in the SourceTree for a given source 1347 * 1348 * @param {object} dbg 1349 * @param {string} sourceName 1350 * Name of the source displayed in the source tree 1351 * @param {string} icon 1352 * Expected icon CSS classname 1353 */ 1354 function assertSourceIcon(dbg, sourceName, icon) { 1355 const sourceItem = findSourceNodeWithText(dbg, sourceName); 1356 ok(sourceItem, `Found the source item for ${sourceName}`); 1357 is( 1358 sourceItem.querySelector(".source-icon").className, 1359 `dbg-img dbg-img-${icon} source-icon`, 1360 `The icon for ${sourceName} is correct` 1361 ); 1362 } 1363 1364 async function expandSourceTree(dbg) { 1365 // Click on expand all context menu for all top level "expandable items". 1366 // If there is no project root, it will be thread items. 1367 // But when there is a project root, it can be directory or group items. 1368 // Select only expandable in order to ignore source items. 1369 for (const rootNode of dbg.win.document.querySelectorAll( 1370 ".sources-list > .tree > .tree-node[data-expandable=true]" 1371 )) { 1372 await expandAllSourceNodes(dbg, rootNode); 1373 } 1374 } 1375 1376 async function expandAllSourceNodes(dbg, treeNode) { 1377 return triggerSourceTreeContextMenu(dbg, treeNode, "#node-menu-expand-all"); 1378 } 1379 1380 /** 1381 * Removes a breakpoint from a source at line/col. 1382 * 1383 * @memberof mochitest/actions 1384 * @param {object} dbg 1385 * @param {string} source 1386 * @param {number} line 1387 * @param {number} col 1388 * @return {Promise} 1389 * @static 1390 */ 1391 function removeBreakpoint(dbg, sourceId, line, column) { 1392 const source = dbg.selectors.getSource(sourceId); 1393 // column is 0-based internally, but tests are using 1-based. 1394 column = column ? column - 1 : getFirstBreakpointColumn(dbg, source, line); 1395 const location = createLocation({ 1396 source, 1397 line, 1398 column, 1399 }); 1400 const bp = getBreakpointForLocation(dbg, location); 1401 return dbg.actions.removeBreakpoint(bp); 1402 } 1403 1404 /** 1405 * Toggles the Pause on exceptions feature in the debugger. 1406 * 1407 * @memberof mochitest/actions 1408 * @param {object} dbg 1409 * @param {boolean} pauseOnExceptions 1410 * @param {boolean} pauseOnCaughtExceptions 1411 * @return {Promise} 1412 * @static 1413 */ 1414 async function togglePauseOnExceptions( 1415 dbg, 1416 pauseOnExceptions, 1417 pauseOnCaughtExceptions 1418 ) { 1419 return dbg.actions.pauseOnExceptions( 1420 pauseOnExceptions, 1421 pauseOnCaughtExceptions 1422 ); 1423 } 1424 1425 // Helpers 1426 1427 /** 1428 * Invokes a global function in the debuggee tab. 1429 * 1430 * @memberof mochitest/helpers 1431 * @param {string} fnc The name of a global function on the content window to 1432 * call. This is applied to structured clones of the 1433 * remaining arguments to invokeInTab. 1434 * @param {Any} ...args Remaining args to serialize and pass to fnc. 1435 * @return {Promise} 1436 * @static 1437 */ 1438 function invokeInTab(fnc, ...args) { 1439 info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`); 1440 return ContentTask.spawn(gBrowser.selectedBrowser, { fnc, args }, options => 1441 content.wrappedJSObject[options.fnc](...options.args) 1442 ); 1443 } 1444 1445 function clickElementInTab(selector) { 1446 info(`click element ${selector} in tab`); 1447 1448 return SpecialPowers.spawn( 1449 gBrowser.selectedBrowser, 1450 [selector], 1451 function (_selector) { 1452 const element = content.document.querySelector(_selector); 1453 // Run the click in another event loop in order to immediately resolve spawn's promise. 1454 // Otherwise if we pause on click and navigate, the JSWindowActor used by spawn will 1455 // be destroyed while its query is still pending. And this would reject the promise. 1456 content.setTimeout(() => { 1457 element.click(); 1458 }); 1459 } 1460 ); 1461 } 1462 1463 const isLinux = Services.appinfo.OS === "Linux"; 1464 const isMac = Services.appinfo.OS === "Darwin"; 1465 const cmdOrCtrl = isMac ? { metaKey: true } : { ctrlKey: true }; 1466 const shiftOrAlt = isMac 1467 ? { accelKey: true, shiftKey: true } 1468 : { accelKey: true, altKey: true }; 1469 1470 const cmdShift = isMac 1471 ? { accelKey: true, shiftKey: true, metaKey: true } 1472 : { accelKey: true, shiftKey: true, ctrlKey: true }; 1473 1474 // On Mac, going to beginning/end only works with meta+left/right. On 1475 // Windows, it only works with home/end. On Linux, apparently, either 1476 // ctrl+left/right or home/end work. 1477 const endKey = isMac 1478 ? { code: "VK_RIGHT", modifiers: cmdOrCtrl } 1479 : { code: "VK_END" }; 1480 const startKey = isMac 1481 ? { code: "VK_LEFT", modifiers: cmdOrCtrl } 1482 : { code: "VK_HOME" }; 1483 1484 const keyMappings = { 1485 close: { code: "w", modifiers: cmdOrCtrl }, 1486 commandKeyDown: { code: "VK_META", modifiers: { type: "keydown" } }, 1487 commandKeyUp: { code: "VK_META", modifiers: { type: "keyup" } }, 1488 debugger: { code: "s", modifiers: shiftOrAlt }, 1489 // test conditional panel shortcut 1490 toggleCondPanel: { code: "b", modifiers: cmdShift }, 1491 toggleLogPanel: { code: "y", modifiers: cmdShift }, 1492 toggleBreakpoint: { code: "b", modifiers: cmdOrCtrl }, 1493 inspector: { code: "c", modifiers: shiftOrAlt }, 1494 quickOpen: { code: "p", modifiers: cmdOrCtrl }, 1495 quickOpenFunc: { code: "o", modifiers: cmdShift }, 1496 quickOpenLine: { code: ":", modifiers: cmdOrCtrl }, 1497 fileSearch: { code: "f", modifiers: cmdOrCtrl }, 1498 projectSearch: { code: "f", modifiers: cmdShift }, 1499 fileSearchNext: { code: "g", modifiers: { metaKey: true } }, 1500 fileSearchPrev: { code: "g", modifiers: cmdShift }, 1501 goToLine: { code: "g", modifiers: { ctrlKey: true } }, 1502 sourceeditorGoToLine: { code: "j", modifiers: cmdOrCtrl }, 1503 Enter: { code: "VK_RETURN" }, 1504 ShiftEnter: { code: "VK_RETURN", modifiers: { shiftKey: true } }, 1505 AltEnter: { 1506 code: "VK_RETURN", 1507 modifiers: { altKey: true }, 1508 }, 1509 Space: { code: "VK_SPACE" }, 1510 Up: { code: "VK_UP" }, 1511 Down: { code: "VK_DOWN" }, 1512 Right: { code: "VK_RIGHT" }, 1513 Left: { code: "VK_LEFT" }, 1514 End: endKey, 1515 Start: startKey, 1516 Tab: { code: "VK_TAB" }, 1517 ShiftTab: { code: "VK_TAB", modifiers: { shiftKey: true } }, 1518 Escape: { code: "VK_ESCAPE" }, 1519 Delete: { code: "VK_DELETE" }, 1520 pauseKey: { code: "VK_F8" }, 1521 resumeKey: { code: "VK_F8" }, 1522 stepOverKey: { code: "VK_F10" }, 1523 stepInKey: { code: "VK_F11" }, 1524 stepOutKey: { 1525 code: "VK_F11", 1526 modifiers: { shiftKey: true }, 1527 }, 1528 Backspace: { code: "VK_BACK_SPACE" }, 1529 }; 1530 1531 /** 1532 * Simulates a key press in the debugger window. 1533 * 1534 * @memberof mochitest/helpers 1535 * @param {object} dbg 1536 * @param {string} keyName 1537 * @return {Promise} 1538 * @static 1539 */ 1540 function pressKey(dbg, keyName) { 1541 const keyEvent = keyMappings[keyName]; 1542 const { code, modifiers } = keyEvent; 1543 info(`The ${keyName} key is pressed`); 1544 return EventUtils.synthesizeKey(code, modifiers || {}, dbg.win); 1545 } 1546 1547 function type(dbg, string) { 1548 string.split("").forEach(char => EventUtils.synthesizeKey(char, {}, dbg.win)); 1549 } 1550 1551 /** 1552 * Checks to see if the inner element is visible inside the editor. 1553 * 1554 * @memberof mochitest/helpers 1555 * @param {object} dbg 1556 * @param {HTMLElement} inner element 1557 * @return {boolean} 1558 * @static 1559 */ 1560 1561 function isVisibleInEditor(dbg, element) { 1562 return isVisible(findElement(dbg, "codeMirror"), element); 1563 } 1564 1565 /** 1566 * Checks to see if the inner element is visible inside the 1567 * outer element. 1568 * 1569 * Note, the inner element does not need to be entirely visible, 1570 * it is possible for it to be somewhat clipped by the outer element's 1571 * bounding element or for it to span the entire length, starting before the 1572 * outer element and ending after. 1573 * 1574 * @memberof mochitest/helpers 1575 * @param {HTMLElement} outer element 1576 * @param {HTMLElement} inner element 1577 * @return {boolean} 1578 * @static 1579 */ 1580 function isVisible(outerEl, innerEl) { 1581 if (!innerEl || !outerEl) { 1582 return false; 1583 } 1584 1585 const innerRect = innerEl.getBoundingClientRect(); 1586 const outerRect = outerEl.getBoundingClientRect(); 1587 1588 const verticallyVisible = 1589 innerRect.top >= outerRect.top || 1590 innerRect.bottom <= outerRect.bottom || 1591 (innerRect.top < outerRect.top && innerRect.bottom > outerRect.bottom); 1592 1593 const horizontallyVisible = 1594 innerRect.left >= outerRect.left || 1595 innerRect.right <= outerRect.right || 1596 (innerRect.left < outerRect.left && innerRect.right > outerRect.right); 1597 1598 const visible = verticallyVisible && horizontallyVisible; 1599 return visible; 1600 } 1601 1602 // Handles virtualization scenarios 1603 async function scrollAndGetEditorLineGutterElement(dbg, line) { 1604 const editor = getCMEditor(dbg); 1605 await scrollEditorIntoView(dbg, line, 0); 1606 const selectedSource = dbg.selectors.getSelectedSource(); 1607 // For WASM sources get the hexadecimal line number displayed in the gutter 1608 if (editor.isWasm && !selectedSource.isOriginal) { 1609 const wasmLineFormatter = editor.getWasmLineNumberFormatter(); 1610 line = wasmLineFormatter(line); 1611 } 1612 1613 const els = findAllElementsWithSelector( 1614 dbg, 1615 ".cm-gutter.cm-lineNumbers .cm-gutterElement" 1616 ); 1617 return [...els].find(el => el.innerText == line); 1618 } 1619 1620 /** 1621 * Gets node at a specific line in the editor 1622 * 1623 * @param {*} dbg 1624 * @param {number} line 1625 * @returns {Element} DOM Element 1626 */ 1627 async function getNodeAtEditorLine(dbg, line) { 1628 await scrollEditorIntoView(dbg, line, 0); 1629 return getCMEditor(dbg).getElementAtLine(line); 1630 } 1631 1632 /** 1633 * Gets node at a specific line in the gutter 1634 * 1635 * @param {*} dbg 1636 * @param {number} line 1637 * @returns {Element} DOM Element 1638 */ 1639 async function getNodeAtEditorGutterLine(dbg, line) { 1640 return scrollAndGetEditorLineGutterElement(dbg, line); 1641 } 1642 1643 async function getConditionalPanelAtLine(dbg, line) { 1644 info(`Get conditional panel at line ${line}`); 1645 const el = await getNodeAtEditorLine(dbg, line); 1646 return el.nextSibling.querySelector(".conditional-breakpoint-panel"); 1647 } 1648 1649 async function waitForConditionalPanelFocus(dbg) { 1650 return waitFor( 1651 () => 1652 dbg.win.document.activeElement.classList.contains("cm-content") && 1653 dbg.win.document.activeElement.closest(".conditional-breakpoint-panel") 1654 ); 1655 } 1656 1657 /** 1658 * Opens the debugger editor context menu in either codemirror or the 1659 * the debugger gutter. 1660 * 1661 * @param {object} dbg 1662 * @param {string} elementName 1663 * The element to select 1664 * @param {number} line 1665 * The line to open the context menu on. 1666 */ 1667 async function openContextMenuInDebugger(dbg, elementName, line) { 1668 const waitForOpen = waitForContextMenu(dbg); 1669 info(`Open ${elementName} context menu on line ${line || ""}`); 1670 rightClickElement(dbg, elementName, line); 1671 return waitForOpen; 1672 } 1673 1674 /** 1675 * Select a range of lines in the editor and open the contextmenu 1676 * 1677 * @param {object} dbg 1678 * @param {object} lines 1679 * @param {string} elementName 1680 * @returns 1681 */ 1682 async function selectEditorLinesAndOpenContextMenu( 1683 dbg, 1684 lines, 1685 elementName = "line" 1686 ) { 1687 const { startLine, endLine } = lines; 1688 setSelection(dbg, startLine, endLine ?? startLine); 1689 return openContextMenuInDebugger(dbg, elementName, startLine); 1690 } 1691 1692 /** 1693 * Asserts that the styling for ignored lines are applied 1694 * 1695 * @param {object} dbg 1696 * @param {object} options 1697 * lines {null | Number[]} [lines] Line(s) to assert. 1698 * - If null is passed, the assertion is on all the blackboxed lines 1699 * - If an array of one item (start line) is passed, the assertion is on the specified line 1700 * - If an array (start and end lines) is passed, the assertion is on the multiple lines seelected 1701 * hasBlackboxedLinesClass 1702 * If `true` assert that style exist, else assert that style does not exist 1703 */ 1704 async function assertIgnoredStyleInSourceLines( 1705 dbg, 1706 { lines, hasBlackboxedLinesClass } 1707 ) { 1708 if (lines) { 1709 let currentLine = lines[0]; 1710 do { 1711 const element = await getNodeAtEditorLine(dbg, currentLine); 1712 const hasStyle = element.classList.contains("blackboxed-line"); 1713 is( 1714 hasStyle, 1715 hasBlackboxedLinesClass, 1716 `Line ${currentLine} ${ 1717 hasBlackboxedLinesClass ? "does not have" : "has" 1718 } ignored styling` 1719 ); 1720 currentLine = currentLine + 1; 1721 } while (currentLine <= lines[1]); 1722 } else { 1723 const codeLines = findAllElements(dbg, "codeLines"); 1724 const blackboxedLines = findAllElements(dbg, "blackboxedLines"); 1725 is( 1726 hasBlackboxedLinesClass ? codeLines.length : 0, 1727 blackboxedLines.length, 1728 `${blackboxedLines.length} of ${codeLines.length} lines are blackboxed` 1729 ); 1730 } 1731 } 1732 1733 /** 1734 * Assert the text content on the line matches what is 1735 * expected. 1736 * 1737 * @param {object} dbg 1738 * @param {number} line 1739 * @param {string} expectedTextContent 1740 */ 1741 function assertTextContentOnLine(dbg, line, expectedTextContent) { 1742 const lineInfo = getCMEditor(dbg).lineInfo(line); 1743 const textContent = lineInfo.text.trim(); 1744 is(textContent, expectedTextContent, `Expected text content on line ${line}`); 1745 } 1746 1747 /** 1748 * Assert that no breakpoint is set on a given line of 1749 * the currently selected source in the editor. 1750 * 1751 * @memberof mochitest/helpers 1752 * @param {object} dbg 1753 * @param {number} line Line where to check for a breakpoint in the editor 1754 * @static 1755 */ 1756 async function assertNoBreakpoint(dbg, line) { 1757 const el = await getNodeAtEditorGutterLine(dbg, line); 1758 1759 const exists = el.classList.contains("cm6-gutter-breakpoint"); 1760 ok(!exists, `Breakpoint doesn't exists on line ${line}`); 1761 } 1762 1763 /** 1764 * Assert that a regular breakpoint is set in the currently 1765 * selected source in the editor. (no conditional, nor log breakpoint) 1766 * 1767 * @memberof mochitest/helpers 1768 * @param {object} dbg 1769 * @param {number} line Line where to check for a breakpoint 1770 * @static 1771 */ 1772 async function assertBreakpoint(dbg, line) { 1773 const el = await getNodeAtEditorGutterLine(dbg, line); 1774 ok( 1775 el.firstChild.classList.contains(selectors.gutterBreakpoint), 1776 `Breakpoint exists on line ${line}` 1777 ); 1778 1779 const hasConditionClass = el.firstChild.classList.contains("has-condition"); 1780 ok( 1781 !hasConditionClass, 1782 `Regular breakpoint doesn't have condition on line ${line}` 1783 ); 1784 1785 const hasLogClass = el.firstChild.classList.contains("has-log"); 1786 ok(!hasLogClass, `Regular breakpoint doesn't have log on line ${line}`); 1787 } 1788 1789 /** 1790 * Assert that a conditionnal breakpoint is set. 1791 * 1792 * @memberof mochitest/helpers 1793 * @param {object} dbg 1794 * @param {number} line Line where to check for a breakpoint 1795 * @static 1796 */ 1797 async function assertConditionBreakpoint(dbg, line) { 1798 const el = await getNodeAtEditorGutterLine(dbg, line); 1799 1800 ok( 1801 el.firstChild.classList.contains(selectors.gutterBreakpoint), 1802 `Breakpoint exists on line ${line}` 1803 ); 1804 1805 const hasConditionClass = el.firstChild.classList.contains("has-condition"); 1806 ok(hasConditionClass, `Conditional breakpoint on line ${line}`); 1807 1808 const hasLogClass = el.firstChild.classList.contains("has-log"); 1809 ok( 1810 !hasLogClass, 1811 `Conditional breakpoint doesn't have log breakpoint on line ${line}` 1812 ); 1813 } 1814 1815 /** 1816 * Assert that a log breakpoint is set. 1817 * 1818 * @memberof mochitest/helpers 1819 * @param {object} dbg 1820 * @param {number} line Line where to check for a breakpoint 1821 * @static 1822 */ 1823 async function assertLogBreakpoint(dbg, line) { 1824 const el = await getNodeAtEditorGutterLine(dbg, line); 1825 ok( 1826 el.firstChild.classList.contains(selectors.gutterBreakpoint), 1827 `Breakpoint exists on line ${line}` 1828 ); 1829 1830 const hasConditionClass = el.firstChild.classList.contains("has-condition"); 1831 ok( 1832 !hasConditionClass, 1833 `Log breakpoint doesn't have condition on line ${line}` 1834 ); 1835 1836 const hasLogClass = el.firstChild.classList.contains("has-log"); 1837 ok(hasLogClass, `Log breakpoint on line ${line}`); 1838 } 1839 1840 function assertBreakpointSnippet(dbg, index, expectedSnippet) { 1841 const actualSnippet = findElement(dbg, "breakpointLabel", 2).innerText; 1842 is(actualSnippet, expectedSnippet, `Breakpoint ${index} snippet`); 1843 } 1844 1845 const selectors = { 1846 callStackBody: ".call-stack-pane .pane", 1847 domMutationItem: ".dom-mutation-list li", 1848 expressionNode: i => 1849 `.expressions-list .expression-container:nth-child(${i}) .object-label`, 1850 expressionValue: i => 1851 // eslint-disable-next-line max-len 1852 `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`, 1853 expressionInput: ".watch-expressions-pane input.input-expression", 1854 expressionNodes: ".expressions-list .tree-node", 1855 expressionPlus: ".watch-expressions-pane button.plus", 1856 expressionRefresh: ".watch-expressions-pane button.refresh", 1857 expressionsHeader: ".watch-expressions-pane ._header .header-label", 1858 scopesHeader: ".scopes-pane ._header .header-label", 1859 breakpointItem: i => `.breakpoints-list div:nth-of-type(${i})`, 1860 breakpointLabel: i => `${selectors.breakpointItem(i)} .breakpoint-label`, 1861 breakpointHeadings: ".breakpoints-list .breakpoint-heading", 1862 breakpointItems: ".breakpoints-list .breakpoint", 1863 breakpointContextMenu: { 1864 disableSelf: "#node-menu-disable-self", 1865 disableAll: "#node-menu-disable-all", 1866 disableOthers: "#node-menu-disable-others", 1867 enableSelf: "#node-menu-enable-self", 1868 enableOthers: "#node-menu-enable-others", 1869 disableDbgStatement: "#node-menu-disable-dbgStatement", 1870 enableDbgStatement: "#node-menu-enable-dbgStatement", 1871 remove: "#node-menu-delete-self", 1872 removeOthers: "#node-menu-delete-other", 1873 removeCondition: "#node-menu-remove-condition", 1874 }, 1875 blackboxedLines: ".cm-content > .blackboxed-line", 1876 codeLines: ".cm-content > .cm-line", 1877 editorContextMenu: { 1878 continueToHere: "#node-menu-continue-to-here", 1879 }, 1880 columnBreakpoints: ".column-breakpoint", 1881 scopes: ".scopes-list", 1882 scopeNodes: ".scopes-list .object-label", 1883 scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`, 1884 scopeValue: i => 1885 `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`, 1886 mapScopesCheckbox: ".map-scopes-header input", 1887 asyncframe: i => 1888 `.frames div[role=listbox] .location-async-cause:nth-child(${i})`, 1889 frame: i => `.frames div[role=listbox] .frame:nth-child(${i})`, 1890 frames: ".frames [role='listbox'] .frame", 1891 gutterBreakpoint: "breakpoint-marker", 1892 // This is used to trigger events (click etc) on the gutter 1893 gutterElement: i => 1894 `.cm-gutter.cm-lineNumbers .cm-gutterElement:nth-child(${i + 1})`, 1895 gutters: `.cm-gutters`, 1896 line: i => `.cm-content > div.cm-line:nth-child(${i})`, 1897 addConditionItem: 1898 "#node-menu-add-condition, #node-menu-add-conditional-breakpoint", 1899 editConditionItem: 1900 "#node-menu-edit-condition, #node-menu-edit-conditional-breakpoint", 1901 addLogItem: "#node-menu-add-log-point", 1902 editLogItem: "#node-menu-edit-log-point", 1903 disableItem: "#node-menu-disable-breakpoint", 1904 breakpoint: ".cm-gutter > .cm6-gutter-breakpoint", 1905 highlightLine: ".cm-content > .highlight-line", 1906 pausedLine: ".paused-line", 1907 tracedLine: ".traced-line", 1908 debugErrorLine: ".new-debug-line-error", 1909 codeMirror: ".cm-editor", 1910 resume: ".resume.active", 1911 pause: ".pause.active", 1912 sourceTabs: ".source-tabs", 1913 activeTab: ".source-tab.active", 1914 stepOver: ".stepOver.active", 1915 stepOut: ".stepOut.active", 1916 stepIn: ".stepIn.active", 1917 prettyPrintButton: ".source-footer .prettyPrint", 1918 mappedSourceLink: ".source-footer .mapped-source", 1919 sourceMapFooterButton: ".debugger-source-map-button", 1920 sourceNode: i => `.sources-list .tree-node:nth-child(${i}) .node`, 1921 sourceNodes: ".sources-list .tree-node", 1922 sourceTreeThreads: '.sources-list .tree-node[aria-level="1"]', 1923 sourceTreeGroups: '.sources-list .tree-node[aria-level="2"]', 1924 sourceTreeFiles: ".sources-list .tree-node[data-expandable=false]", 1925 sourceTreeFilesElement: i => 1926 `.sources-list .tree-node[data-expandable=false]:nth-child(${i})`, 1927 threadSourceTree: i => `.threads-list .sources-pane:nth-child(${i})`, 1928 sourceDirectoryLabel: i => `.sources-list .tree-node:nth-child(${i}) .label`, 1929 resultItems: ".result-list .result-item", 1930 resultItemName: (name, i) => 1931 `${selectors.resultItems}:nth-child(${i})[title$="${name}"]`, 1932 fileMatch: ".project-text-search .line-value", 1933 popup: ".popover", 1934 previewPopup: ".preview-popup", 1935 openInspector: "button.open-inspector", 1936 outlineItem: i => 1937 `.outline-list__element:nth-child(${i}) .function-signature`, 1938 outlineItems: ".outline-list__element", 1939 conditionalPanel: ".conditional-breakpoint-panel", 1940 conditionalPanelInput: `.conditional-breakpoint-panel .cm-content`, 1941 logPanelInput: `.conditional-breakpoint-panel.log-point .cm-content`, 1942 conditionalBreakpointInSecPane: ".breakpoint.is-conditional", 1943 logPointPanel: ".conditional-breakpoint-panel.log-point", 1944 logPointInSecPane: ".breakpoint.is-log", 1945 tracePanel: ".trace-panel", 1946 searchField: ".search-field", 1947 blackbox: ".action.black-box", 1948 projectSearchSearchInput: ".project-text-search .search-field input", 1949 projectSearchCollapsed: ".project-text-search .dbg-img-arrow:not(.expanded)", 1950 projectSearchExpandedResults: ".project-text-search .result", 1951 projectSearchFileResults: ".project-text-search .file-result", 1952 projectSearchModifiersCaseSensitive: 1953 ".project-text-search button.case-sensitive-btn", 1954 projectSearchModifiersRegexMatch: 1955 ".project-text-search button.regex-match-btn", 1956 projectSearchModifiersWholeWordMatch: 1957 ".project-text-search button.whole-word-btn", 1958 projectSearchRefreshButton: ".project-text-search button.refresh-btn", 1959 threadsPaneItems: ".threads-pane .thread", 1960 threadsPaneItem: i => `.threads-pane .thread:nth-child(${i})`, 1961 threadsPaneItemPause: i => `${selectors.threadsPaneItem(i)}.paused`, 1962 CodeMirrorLines: ".cm-content", 1963 CodeMirrorCode: ".cm-content", 1964 visibleInlinePreviews: ".inline-preview .inline-preview-outer", 1965 inlinePreviewsOnLine: i => 1966 `.cm-content > div.cm-line:nth-child(${i}) .inline-preview .inline-preview-outer`, 1967 inlinePreviewOpenInspector: ".inline-preview-value button.open-inspector", 1968 watchpointsSubmenu: "#node-menu-watchpoints", 1969 addGetWatchpoint: "#node-menu-add-get-watchpoint", 1970 logEventsCheckbox: ".events-header input", 1971 previewPopupInvokeGetterButton: ".preview-popup .invoke-getter", 1972 previewPopupObjectNumber: ".preview-popup .objectBox-number", 1973 previewPopupObjectObject: ".preview-popup .objectBox-object", 1974 previewPopupObjectFunction: ".preview-popup .objectBox-function", 1975 previewPopupObjectFunctionJumpToDefinition: 1976 ".preview-popup .objectBox-function .jump-definition", 1977 sourceTreeRootNode: ".sources-panel .node .dbg-img-window", 1978 sourceTreeFolderNode: ".sources-panel .node .dbg-img-folder", 1979 excludePatternsInput: ".project-text-search .exclude-patterns-field input", 1980 fileSearchInput: ".search-bar input", 1981 fileSearchSummary: ".search-bar .search-field-summary", 1982 watchExpressionsHeader: ".watch-expressions-pane ._header .header-label", 1983 watchExpressionsAddButton: ".watch-expressions-pane ._header .plus", 1984 editorNotificationFooter: ".editor-notification-footer", 1985 }; 1986 1987 function getSelector(elementName, ...args) { 1988 let selector = selectors[elementName]; 1989 if (!selector) { 1990 throw new Error(`The selector ${elementName} is not defined`); 1991 } 1992 1993 if (typeof selector == "function") { 1994 selector = selector(...args); 1995 } 1996 1997 return selector; 1998 } 1999 2000 function findElement(dbg, elementName, ...args) { 2001 const selector = getSelector(elementName, ...args); 2002 return findElementWithSelector(dbg, selector); 2003 } 2004 2005 function findElementWithSelector(dbg, selector) { 2006 return dbg.win.document.querySelector(selector); 2007 } 2008 2009 function findAllElements(dbg, elementName, ...args) { 2010 const selector = getSelector(elementName, ...args); 2011 return findAllElementsWithSelector(dbg, selector); 2012 } 2013 2014 function findAllElementsWithSelector(dbg, selector) { 2015 return dbg.win.document.querySelectorAll(selector); 2016 } 2017 2018 function getSourceNodeLabel(dbg, index) { 2019 return findElement(dbg, "sourceNode", index) 2020 .textContent.trim() 2021 .replace(/^[\s\u200b]*/g, ""); 2022 } 2023 2024 /** 2025 * Simulates a mouse click in the debugger DOM. 2026 * 2027 * @memberof mochitest/helpers 2028 * @param {object} dbg 2029 * @param {string} elementName 2030 * @param {Array} args 2031 * @return {Promise} 2032 * @static 2033 */ 2034 async function clickElement(dbg, elementName, ...args) { 2035 const selector = getSelector(elementName, ...args); 2036 const el = await waitForElementWithSelector(dbg, selector); 2037 2038 el.scrollIntoView(); 2039 2040 return clickElementWithSelector(dbg, selector); 2041 } 2042 2043 function clickElementWithSelector(dbg, selector) { 2044 clickDOMElement(dbg, findElementWithSelector(dbg, selector)); 2045 } 2046 2047 function clickDOMElement(dbg, element, options = {}) { 2048 EventUtils.synthesizeMouseAtCenter(element, options, dbg.win); 2049 } 2050 2051 function dblClickElement(dbg, elementName, ...args) { 2052 const selector = getSelector(elementName, ...args); 2053 2054 return EventUtils.synthesizeMouseAtCenter( 2055 findElementWithSelector(dbg, selector), 2056 { clickCount: 2 }, 2057 dbg.win 2058 ); 2059 } 2060 2061 function clickElementWithOptions(dbg, elementName, options, ...args) { 2062 const selector = getSelector(elementName, ...args); 2063 const el = findElementWithSelector(dbg, selector); 2064 el.scrollIntoView(); 2065 2066 return EventUtils.synthesizeMouseAtCenter(el, options, dbg.win); 2067 } 2068 2069 function altClickElement(dbg, elementName, ...args) { 2070 return clickElementWithOptions(dbg, elementName, { altKey: true }, ...args); 2071 } 2072 2073 function shiftClickElement(dbg, elementName, ...args) { 2074 return clickElementWithOptions(dbg, elementName, { shiftKey: true }, ...args); 2075 } 2076 2077 function rightClickElement(dbg, elementName, ...args) { 2078 const selector = getSelector(elementName, ...args); 2079 return rightClickEl(dbg, dbg.win.document.querySelector(selector)); 2080 } 2081 2082 function rightClickEl(dbg, el) { 2083 el.scrollIntoView(); 2084 EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win); 2085 } 2086 2087 async function clearElement(dbg, elementName) { 2088 await clickElement(dbg, elementName); 2089 await pressKey(dbg, "End"); 2090 const selector = getSelector(elementName); 2091 const el = findElementWithSelector(dbg, getSelector(elementName)); 2092 let len = el.value.length; 2093 while (len) { 2094 pressKey(dbg, "Backspace"); 2095 len--; 2096 } 2097 } 2098 2099 async function clickGutter(dbg, line) { 2100 const el = await scrollAndGetEditorLineGutterElement(dbg, line); 2101 clickDOMElement(dbg, el); 2102 } 2103 2104 async function cmdClickGutter(dbg, line) { 2105 const el = await scrollAndGetEditorLineGutterElement(dbg, line); 2106 clickDOMElement(dbg, el, cmdOrCtrl); 2107 } 2108 2109 function findContextMenu(dbg, selector) { 2110 // the context menu is in the toolbox window 2111 const doc = dbg.toolbox.topDoc; 2112 2113 // there are several context menus, we want the one with the menu-api 2114 const popup = doc.querySelector('menupopup[menu-api="true"]'); 2115 2116 return popup.querySelector(selector); 2117 } 2118 2119 async function assertContextMenuItemDisabled(dbg, selector, expectedState) { 2120 const item = await waitFor(() => findContextMenu(dbg, selector)); 2121 is(item.disabled, expectedState, "The context menu item is disabled"); 2122 } 2123 2124 // Waits for the context menu to exist and to fully open. Once this function 2125 // completes, selectDebuggerContextMenuItem can be called. 2126 // waitForContextMenu must be called after menu opening has been triggered, e.g. 2127 // after synthesizing a right click / contextmenu event. 2128 async function waitForContextMenu(dbg) { 2129 // the context menu is in the toolbox window 2130 const doc = dbg.toolbox.topDoc; 2131 2132 // there are several context menus, we want the one with the menu-api 2133 const popup = await waitFor(() => 2134 doc.querySelector('menupopup[menu-api="true"]') 2135 ); 2136 2137 if (popup.state == "open") { 2138 return popup; 2139 } 2140 2141 await new Promise(resolve => { 2142 popup.addEventListener("popupshown", () => resolve(), { once: true }); 2143 }); 2144 2145 return popup; 2146 } 2147 2148 /** 2149 * Closes and open context menu popup. 2150 * 2151 * @memberof mochitest/helpers 2152 * @param {object} dbg 2153 * @param {string} popup - The currently opened popup returned by 2154 * `waitForContextMenu`. 2155 * @return {Promise} 2156 */ 2157 2158 async function closeContextMenu(dbg, popup) { 2159 const onHidden = new Promise(resolve => { 2160 popup.addEventListener("popuphidden", resolve, { once: true }); 2161 }); 2162 popup.hidePopup(); 2163 return onHidden; 2164 } 2165 2166 function selectDebuggerContextMenuItem(dbg, selector) { 2167 const item = findContextMenu(dbg, selector); 2168 item.closest("menupopup").activateItem(item); 2169 } 2170 2171 async function openContextMenuSubmenu(dbg, selector) { 2172 const item = findContextMenu(dbg, selector); 2173 const popup = item.menupopup; 2174 const popupshown = new Promise(resolve => { 2175 popup.addEventListener("popupshown", () => resolve(), { once: true }); 2176 }); 2177 item.openMenu(true); 2178 await popupshown; 2179 return popup; 2180 } 2181 2182 async function assertContextMenuLabel(dbg, selector, expectedLabel) { 2183 const item = await waitFor(() => findContextMenu(dbg, selector)); 2184 is( 2185 item.label, 2186 expectedLabel, 2187 "The label of the context menu item shown to the user" 2188 ); 2189 } 2190 2191 async function typeInPanel(dbg, text, inLogPanel = false) { 2192 const panelName = inLogPanel ? "logPanelInput" : "conditionalPanelInput"; 2193 await waitForElement(dbg, panelName); 2194 2195 // Wait a bit for panel's codemirror document to complete any updates 2196 // so the input does not lose focus after the it has been opened 2197 await waitForInPanelDocumentLoadComplete(dbg, panelName); 2198 2199 // Position cursor reliably at the end of the text. 2200 pressKey(dbg, "End"); 2201 2202 type(dbg, text); 2203 // Wait for any possible scroll actions in the conditional panel editor to complete 2204 await wait(1000); 2205 pressKey(dbg, "Enter"); 2206 } 2207 2208 async function toggleMapScopes(dbg) { 2209 info("Turn on original variable mapping"); 2210 const scopesLoaded = waitForLoadedScopes(dbg); 2211 const onDispatch = waitForDispatch(dbg.store, "TOGGLE_MAP_SCOPES"); 2212 clickElement(dbg, "mapScopesCheckbox"); 2213 return Promise.all([onDispatch, scopesLoaded]); 2214 } 2215 2216 async function waitForPausedInOriginalFileAndToggleMapScopes( 2217 dbg, 2218 expectedSelectedSource = null 2219 ) { 2220 // Original variable mapping is not switched on, so do not wait for any loaded scopes 2221 await waitForPaused(dbg, expectedSelectedSource, { 2222 shouldWaitForLoadedScopes: false, 2223 }); 2224 await toggleMapScopes(dbg); 2225 } 2226 2227 function toggleExpressions(dbg) { 2228 return findElement(dbg, "expressionsHeader").click(); 2229 } 2230 2231 function toggleScopes(dbg) { 2232 return findElement(dbg, "scopesHeader").click(); 2233 } 2234 2235 function toggleExpressionNode(dbg, index) { 2236 return toggleObjectInspectorNode(findElement(dbg, "expressionNode", index)); 2237 } 2238 2239 function toggleScopeNode(dbg, index) { 2240 return toggleObjectInspectorNode(findElement(dbg, "scopeNode", index)); 2241 } 2242 2243 function rightClickScopeNode(dbg, index) { 2244 rightClickObjectInspectorNode(dbg, findElement(dbg, "scopeNode", index)); 2245 } 2246 2247 function getScopeNodeLabel(dbg, index) { 2248 return findElement(dbg, "scopeNode", index).innerText; 2249 } 2250 2251 function getScopeNodeValue(dbg, index) { 2252 return findElement(dbg, "scopeValue", index).innerText; 2253 } 2254 2255 function toggleObjectInspectorNode(node) { 2256 const objectInspector = node.closest(".object-inspector"); 2257 const properties = objectInspector.querySelectorAll(".node").length; 2258 2259 info(`Toggle node ${node.innerText}`); 2260 node.click(); 2261 2262 info(`Waiting for object inspector properties update`); 2263 return waitUntil( 2264 () => objectInspector.querySelectorAll(".node").length !== properties 2265 ); 2266 } 2267 2268 function rightClickObjectInspectorNode(dbg, node) { 2269 const objectInspector = node.closest(".object-inspector"); 2270 const properties = objectInspector.querySelectorAll(".node").length; 2271 2272 info(`Right clicking node ${node.innerText}`); 2273 rightClickEl(dbg, node); 2274 2275 info(`Waiting for object inspector properties update`); 2276 return waitUntil( 2277 () => objectInspector.querySelectorAll(".node").length !== properties 2278 ); 2279 } 2280 2281 /******************************************* 2282 * Utilities for handling codemirror 2283 ******************************************/ 2284 2285 // Gets the current source editor for CM6 tests 2286 function getCMEditor(dbg) { 2287 return dbg.win.codeMirrorSourceEditorTestInstance; 2288 } 2289 2290 function wasmOffsetToLine(dbg, offset) { 2291 return getCMEditor(dbg).wasmOffsetToLine(offset) + 1; 2292 } 2293 2294 // Gets the number of lines in the editor 2295 function getLineCount(dbg) { 2296 return getCMEditor(dbg).getLineCount(); 2297 } 2298 2299 /** 2300 * Wait for CodeMirror to start searching 2301 */ 2302 function waitForSearchState(dbg) { 2303 return waitFor(() => getCMEditor(dbg).isSearchStateReady()); 2304 } 2305 2306 /** 2307 * Wait for the document of the main debugger editor codemirror instance 2308 * to completely load (for CM6 only) 2309 */ 2310 function waitForDocumentLoadComplete(dbg) { 2311 return waitFor(() => getCMEditor(dbg).codeMirror.isDocumentLoadComplete); 2312 } 2313 2314 /** 2315 * Wait for the document of the conditional/log point panel's codemirror instance 2316 * to completely load (for CM6 only) 2317 */ 2318 function waitForInPanelDocumentLoadComplete(dbg, panelName) { 2319 return waitFor( 2320 () => getCodeMirrorInstance(dbg, panelName).isDocumentLoadComplete 2321 ); 2322 } 2323 2324 /** 2325 * Gets the content for the editor as a string. it uses the 2326 * newline character to separate lines. 2327 */ 2328 function getEditorContent(dbg) { 2329 return getCMEditor(dbg).getEditorContent(); 2330 } 2331 2332 /** 2333 * Retrieve the codemirror instance for the provided debugger instance. 2334 * Optionally provide a panel name such as "logPanelInput" or 2335 * "conditionalPanelInput" to retrieve the codemirror instances specific to 2336 * those panels. 2337 * 2338 * @param {object} dbg 2339 * @param {string} panelName 2340 * @returns {CodeMirror} 2341 * The codemirror instance corresponding to the provided debugger and panel name. 2342 */ 2343 function getCodeMirrorInstance(dbg, panelName = null) { 2344 if (panelName !== null) { 2345 const panel = findElement(dbg, panelName); 2346 return dbg.win.codeMirrorSourceEditorTestInstance.CodeMirror.findFromDOM( 2347 panel 2348 ); 2349 } 2350 return dbg.win.codeMirrorSourceEditorTestInstance.codeMirror; 2351 } 2352 2353 async function waitForCursorPosition(dbg, expectedLine) { 2354 return waitFor(() => { 2355 const cursorPosition = findElementWithSelector(dbg, ".cursor-position"); 2356 if (!cursorPosition) { 2357 return false; 2358 } 2359 const { innerText } = cursorPosition; 2360 // Cursor position text has the following shape: (L, C) 2361 // where L is the line number, and C the column number 2362 const line = innerText.substring(1, innerText.indexOf(",")); 2363 return parseInt(line, 10) == expectedLine; 2364 }); 2365 } 2366 2367 /** 2368 * Set the cursor at a specific location in the editor 2369 * 2370 * @param {*} dbg 2371 * @param {number} line 2372 * @param {number} column 2373 * @returns {Promise} 2374 */ 2375 async function setEditorCursorAt(dbg, line, column) { 2376 const cursorSet = waitForCursorPosition(dbg, line); 2377 await getCMEditor(dbg).setCursorAt(line, column); 2378 return cursorSet; 2379 } 2380 2381 /** 2382 * Scrolls a specific line and column into view in the editor 2383 * 2384 * @param {*} dbg 2385 * @param {number} line 2386 * @param {number} column 2387 * @param {string | null} yAlign 2388 * @returns 2389 */ 2390 async function scrollEditorIntoView(dbg, line, column, yAlign) { 2391 const onScrolled = waitForScrolling(dbg); 2392 getCMEditor(dbg).scrollTo(line + 1, column, yAlign); 2393 // Ensure the line is visible with margin because the bar at the bottom of 2394 // the editor overlaps into what the editor thinks is its own space, blocking 2395 // the click event below. 2396 return onScrolled; 2397 } 2398 2399 /** 2400 * Wrapper around source editor api to check if a scrolled position is visible 2401 * 2402 * @param {*} dbg 2403 * @param {number} line 1-based 2404 * @param {number} column 2405 * @returns 2406 */ 2407 function isScrolledPositionVisible(dbg, line, column = 0) { 2408 // CodeMirror 6 uses 1-based lines. 2409 return getCMEditor(dbg).isPositionVisible(line, column); 2410 } 2411 2412 function setSelection(dbg, startLine, endLine) { 2413 getCMEditor(dbg).setSelectionAt( 2414 { line: startLine, column: 0 }, 2415 { line: endLine, column: 0 } 2416 ); 2417 } 2418 2419 function getSearchQuery(dbg) { 2420 return getCMEditor(dbg).getSearchQuery(); 2421 } 2422 2423 function getSearchSelection(dbg) { 2424 return getCMEditor(dbg).getSearchSelection(); 2425 } 2426 2427 // Gets the mode used for the file 2428 function getEditorFileMode(dbg) { 2429 return getCMEditor(dbg).getEditorFileMode(); 2430 } 2431 2432 function getCoordsFromPosition(dbg, line, ch) { 2433 return getCMEditor(dbg).getCoords(line, ch); 2434 } 2435 2436 async function getTokenFromPosition(dbg, { line, column = 0 }) { 2437 info(`Get token at ${line}:${column}`); 2438 await scrollEditorIntoView(dbg, line, column); 2439 return getCMEditor(dbg).getElementAtPos(line, column); 2440 } 2441 /** 2442 * Waits for the currently triggered scroll to complete 2443 * 2444 * @param {*} dbg 2445 * @param {object} options 2446 * @param {boolean} options.useTimeoutFallback - defaults to true. When set to false 2447 * a scroll must happen for the wait for scrolling to complete 2448 * @returns 2449 */ 2450 async function waitForScrolling(dbg, { useTimeoutFallback = true } = {}) { 2451 return new Promise(resolve => { 2452 const editor = getCMEditor(dbg); 2453 editor.once("cm-editor-scrolled", resolve); 2454 if (useTimeoutFallback) { 2455 setTimeout(resolve, 500); 2456 } 2457 }); 2458 } 2459 2460 async function clickAtPos(dbg, pos) { 2461 const tokenEl = await getTokenFromPosition(dbg, pos); 2462 2463 if (!tokenEl) { 2464 return; 2465 } 2466 2467 const { top, left } = tokenEl.getBoundingClientRect(); 2468 info( 2469 `Clicking on token ${tokenEl.innerText} in line ${tokenEl.parentNode.innerText}` 2470 ); 2471 // TODO: Unify the usage for CM6 and CM5 Bug 1919694 2472 EventUtils.synthesizeMouseAtCenter(tokenEl, {}, dbg.win); 2473 } 2474 2475 async function rightClickAtPos(dbg, pos) { 2476 const el = await getTokenFromPosition(dbg, pos); 2477 if (!el) { 2478 return; 2479 } 2480 // In CM6 when clicking in the editor an extra click is needed 2481 // TODO: Investiage and remove Bug 1919693 2482 EventUtils.synthesizeMouseAtCenter(el, {}, dbg.win); 2483 rightClickEl(dbg, el); 2484 } 2485 2486 async function hoverAtPos(dbg, pos) { 2487 const tokenEl = await getTokenFromPosition(dbg, pos); 2488 2489 if (!tokenEl) { 2490 return; 2491 } 2492 2493 hoverToken(tokenEl); 2494 } 2495 2496 function hoverToken(tokenEl) { 2497 info(`Hovering on token <${tokenEl.innerText}>`); 2498 2499 // We can't use synthesizeMouse(AtCenter) as it's using the element bounding client rect. 2500 // But here, we might have a token that wraps on multiple line and the center of the 2501 // bounding client rect won't actually hover the token. 2502 // +───────────────────────+ 2503 // │ myLongVariableNa│ 2504 // │me + │ 2505 // +───────────────────────+ 2506 2507 // Instead, we need to get the first quad. 2508 const { p1, p2, p3 } = tokenEl.getBoxQuads()[0]; 2509 const x = p1.x + (p2.x - p1.x) / 2; 2510 const y = p1.y + (p3.y - p1.y) / 2; 2511 2512 // This first event helps utils/editor/tokens.js to receive the right mouseover event 2513 EventUtils.synthesizeMouseAtPoint( 2514 x, 2515 y, 2516 { 2517 type: "mouseover", 2518 }, 2519 tokenEl.ownerGlobal 2520 ); 2521 2522 // This second event helps Popover to have :hover pseudoclass set on the token element 2523 EventUtils.synthesizeMouseAtPoint( 2524 x, 2525 y, 2526 { 2527 type: "mousemove", 2528 }, 2529 tokenEl.ownerGlobal 2530 ); 2531 } 2532 2533 /** 2534 * Helper to close a variable preview popup. 2535 * 2536 * @param {object} dbg 2537 * @param {DOM Element} tokenEl 2538 * The DOM element on which we hovered to display the popup. 2539 * @param {string} previewType 2540 * Based on the actual JS value being hovered we may have two different kinds 2541 * of popups: popup (for js objects) or previewPopup (for primitives) 2542 */ 2543 async function closePreviewForToken( 2544 dbg, 2545 tokenEl, 2546 previewType = "previewPopup" 2547 ) { 2548 ok( 2549 findElement(dbg, previewType), 2550 "A preview was opened before trying to close it" 2551 ); 2552 2553 // Force "mousing out" from all elements. 2554 // 2555 // This helps utils/editor/tokens.js to receive the right mouseleave event. 2556 // This is super important as it will then allow re-emitting a tokenenter event if you try to re-preview the same token! 2557 // We can't use synthesizeMouse(AtCenter) as it's using the element bounding client rect. 2558 // But here, we might have a token that wraps on multiple line and the center of the 2559 // bounding client rect won't actually hover the token. 2560 // +───────────────────────+ 2561 // │ myLongVariableNa│ 2562 // │me + │ 2563 // +───────────────────────+ 2564 2565 // Instead, we need to get the first quad. 2566 const { p1, p2, p3 } = tokenEl.getBoxQuads()[0]; 2567 const x = p1.x + (p2.x - p1.x) / 2; 2568 const y = p1.y + (p3.y - p1.y) / 2; 2569 EventUtils.synthesizeMouseAtPoint( 2570 tokenEl, 2571 x, 2572 y, 2573 { 2574 type: "mouseout", 2575 }, 2576 tokenEl.ownerGlobal 2577 ); 2578 2579 // This second event helps Popover to have :hover pseudoclass removed on the token element 2580 // 2581 // For some unexplained reason, the precise element onto which we emit mousemove is actually important. 2582 // Emitting it on documentElement, or random other element within CodeMirror would cause 2583 // a "mousemove" event to be emitted on preview-popup element instead and wouldn't cause :hover 2584 // pseudoclass to be dropped. 2585 const element = tokenEl.ownerDocument.querySelector( 2586 ".debugger-settings-menu-button" 2587 ); 2588 EventUtils.synthesizeMouseAtCenter( 2589 element, 2590 { 2591 type: "mousemove", 2592 }, 2593 element.ownerGlobal 2594 ); 2595 2596 info(`Waiting for preview to be closed (preview type=${previewType})`); 2597 await waitUntil(() => findElement(dbg, previewType) == null); 2598 info("Preview closed"); 2599 } 2600 2601 /** 2602 * Hover at a position until we see a preview element (popup, tooltip) appear. 2603 * ⚠️ Note that this is using CodeMirror method to retrieve the token element 2604 * and that could be subject to CodeMirror bugs / outdated internal state 2605 * 2606 * @param {Debugger} dbg 2607 * @param {Integer} line: The line we want to hover over 2608 * @param {Integer} column: The column we want to hover over 2609 * @param {string} elementName: "Selector" string that will be passed to waitForElement, 2610 * describing the element that should be displayed on hover. 2611 * @returns Promise<{element, tokenEl}> 2612 * element is the DOM element matching the passed elementName 2613 * tokenEl is the DOM element for the token we hovered 2614 */ 2615 async function tryHovering(dbg, line, column, elementName) { 2616 ok( 2617 !findElement(dbg, elementName), 2618 "The expected preview element on hover should not exist beforehand" 2619 ); 2620 // Wait for all the updates to the document to complete to make all 2621 // token elements have been rendered 2622 await waitForDocumentLoadComplete(dbg); 2623 const tokenEl = await getTokenFromPosition(dbg, { line, column }); 2624 return tryHoverToken(dbg, tokenEl, elementName); 2625 } 2626 2627 /** 2628 * Retrieve the token element matching `expression` at line `line` and hover it. 2629 * This is retrieving the token from the DOM, contrary to `tryHovering`, which calls 2630 * CodeMirror internal method for this (and which might suffer from bugs / outdated internal state) 2631 * 2632 * @param {Debugger} dbg 2633 * @param {string} expression: The text of the token we want to hover 2634 * @param {Integer} line: The line the token should be at 2635 * @param {Integer} column: The column the token should be at 2636 * @param {string} elementName: "Selector" string that will be passed to waitForElement, 2637 * describing the element that should be displayed on hover. 2638 * @returns Promise<{element, tokenEl}> 2639 * element is the DOM element matching the passed elementName 2640 * tokenEl is the DOM element for the token we hovered 2641 */ 2642 async function tryHoverTokenAtLine(dbg, expression, line, column, elementName) { 2643 info("Scroll codeMirror to make the token visible"); 2644 await scrollEditorIntoView(dbg, line, 0); 2645 // Wait for all the updates to the document to complete to make all 2646 // token elements have been rendered 2647 await waitForDocumentLoadComplete(dbg); 2648 // Lookup for the token matching the passed expression 2649 const tokenEl = await getTokenElAtLine(dbg, expression, line, column); 2650 if (!tokenEl) { 2651 throw new Error( 2652 `Couldn't find token <${expression}> on ${line}:${column}\n` 2653 ); 2654 } 2655 2656 ok(true, `Found token <${expression}> on ${line}:${column}`); 2657 2658 return tryHoverToken(dbg, tokenEl, elementName); 2659 } 2660 2661 async function tryHoverToken(dbg, tokenEl, elementName) { 2662 hoverToken(tokenEl); 2663 2664 // Wait for the preview element to be created 2665 const element = await waitForElement(dbg, elementName); 2666 return { element, tokenEl }; 2667 } 2668 2669 /** 2670 * Retrieve the token element matching `expression` at line `line`, from the DOM. 2671 * 2672 * @param {Debugger} dbg 2673 * @param {string} expression: The text of the token we want to hover 2674 * @param {Integer} line: The line the token should be at 2675 * @param {Integer} column: The column the token should be at 2676 * @returns {Element} the token element, or null if not found 2677 */ 2678 async function getTokenElAtLine(dbg, expression, line, column = 0) { 2679 info(`Search for <${expression}> token on ${line}:${column}`); 2680 // Get the related editor line 2681 const tokenParent = getCMEditor(dbg).getElementAtLine(line); 2682 2683 // Lookup for the token matching the passed expression 2684 const tokenElements = [...tokenParent.childNodes]; 2685 let currentColumn = 1; 2686 return tokenElements.find(el => { 2687 const childText = el.textContent; 2688 currentColumn += childText.length; 2689 2690 // Only consider elements that are after the passed column 2691 if (currentColumn < column) { 2692 return false; 2693 } 2694 return childText == expression; 2695 }); 2696 } 2697 2698 /** 2699 * Wait for a few ms and assert that a tooltip preview was not displayed. 2700 * 2701 * @param {*} dbg 2702 */ 2703 async function assertNoTooltip(dbg) { 2704 await wait(200); 2705 const el = findElement(dbg, "previewPopup"); 2706 is(el, null, "Tooltip should not exist"); 2707 } 2708 2709 /** 2710 * Hovers and asserts tooltip previews with simple text expressions (i.e numbers and strings) 2711 * 2712 * @param {*} dbg 2713 * @param {number} line 2714 * @param {number} column 2715 * @param {object} options 2716 * @param {string} options.result - Expected text shown in the preview 2717 * @param {string} options.expression - The expression hovered over 2718 * @param {boolean} options.doNotClose - Set to true to not close the tooltip 2719 */ 2720 async function assertPreviewTextValue( 2721 dbg, 2722 line, 2723 column, 2724 { result, expression, doNotClose = false } 2725 ) { 2726 // CodeMirror refreshes after inline previews are displayed, so wait until they're rendered. 2727 await waitForInlinePreviews(dbg); 2728 2729 const { element: previewEl, tokenEl } = await tryHoverTokenAtLine( 2730 dbg, 2731 expression, 2732 line, 2733 column, 2734 "previewPopup" 2735 ); 2736 2737 ok( 2738 previewEl.innerText.includes(result), 2739 "Popup preview text shown to user. Got: " + 2740 previewEl.innerText + 2741 " Expected: " + 2742 result 2743 ); 2744 2745 if (!doNotClose) { 2746 await closePreviewForToken(dbg, tokenEl); 2747 } 2748 } 2749 2750 /** 2751 * Asserts multiple previews 2752 * 2753 * @param {*} dbg 2754 * @param {Array} previews 2755 */ 2756 async function assertPreviews(dbg, previews) { 2757 // Move the cursor to the top left corner to have a clean state 2758 EventUtils.synthesizeMouse( 2759 findElement(dbg, "codeMirror"), 2760 0, 2761 0, 2762 { 2763 type: "mousemove", 2764 }, 2765 dbg.win 2766 ); 2767 2768 // CodeMirror refreshes after inline previews are displayed, so wait until they're rendered. 2769 await waitForInlinePreviews(dbg); 2770 2771 for (const { line, column, expression, result, header, fields } of previews) { 2772 info(" # Assert preview on " + line + ":" + column); 2773 2774 if (result) { 2775 await assertPreviewTextValue(dbg, line, column, { 2776 expression, 2777 result, 2778 }); 2779 } 2780 2781 if (fields) { 2782 const { element: popupEl, tokenEl } = expression 2783 ? await tryHoverTokenAtLine(dbg, expression, line, column, "popup") 2784 : await tryHovering(dbg, line, column, "popup"); 2785 2786 info("Wait for child nodes to load"); 2787 await waitUntil( 2788 () => popupEl.querySelectorAll(".preview-popup .node").length > 1 2789 ); 2790 ok(true, "child nodes loaded"); 2791 2792 const oiNodes = Array.from( 2793 popupEl.querySelectorAll(".preview-popup .node") 2794 ); 2795 2796 if (header) { 2797 is( 2798 oiNodes[0].querySelector(".objectBox").textContent, 2799 header, 2800 "popup has expected value" 2801 ); 2802 } 2803 2804 for (const [field, value] of fields) { 2805 const node = oiNodes.find( 2806 oiNode => oiNode.querySelector(".object-label")?.textContent === field 2807 ); 2808 if (!node) { 2809 ok(false, `The "${field}" property is not displayed in the popup`); 2810 } else { 2811 is( 2812 node.querySelector(".object-label").textContent, 2813 field, 2814 `The "${field}" property is displayed in the popup` 2815 ); 2816 if (value !== undefined) { 2817 is( 2818 node.querySelector(".objectBox").textContent, 2819 value, 2820 `The "${field}" property has the expected value` 2821 ); 2822 } 2823 } 2824 } 2825 2826 await closePreviewForToken(dbg, tokenEl, "popup"); 2827 } 2828 } 2829 } 2830 2831 /** 2832 * Asserts the inline expression preview value 2833 * 2834 * @param {*} dbg 2835 * @param {number} line 2836 * @param {number} column 2837 * @param {object} options 2838 * @param {string} options.result - Expected text shown in the preview 2839 * @param {Array} options.fields - The expected stacktrace information 2840 */ 2841 async function assertInlineExceptionPreview( 2842 dbg, 2843 line, 2844 column, 2845 { result, fields } 2846 ) { 2847 info(" # Assert preview on " + line + ":" + column); 2848 const { element: popupEl, tokenEl } = await tryHovering( 2849 dbg, 2850 line, 2851 column, 2852 "previewPopup" 2853 ); 2854 2855 info("Wait for top level node to expand and child nodes to load"); 2856 await waitForElementWithSelector( 2857 dbg, 2858 ".exception-popup .exception-message .dbg-img-arrow.expanded" 2859 ); 2860 2861 is( 2862 popupEl.querySelector(".preview-popup .exception-message .objectBox") 2863 .textContent, 2864 result, 2865 "The correct result is not displayed in the popup" 2866 ); 2867 2868 await waitFor(() => 2869 popupEl.querySelectorAll(".preview-popup .exception-stacktrace .frame") 2870 ); 2871 const stackFrameNodes = Array.from( 2872 popupEl.querySelectorAll(".preview-popup .exception-stacktrace .frame") 2873 ); 2874 2875 for (const [field, value] of fields) { 2876 const node = stackFrameNodes.find( 2877 frameNode => frameNode.querySelector(".title")?.textContent === field 2878 ); 2879 if (!node) { 2880 ok(false, `The "${field}" property is not displayed in the popup`); 2881 } else { 2882 is( 2883 node.querySelector(".location").textContent, 2884 value, 2885 `The "${field}" property has the expected value` 2886 ); 2887 } 2888 } 2889 2890 await closePreviewForToken(dbg, tokenEl, "previewPopup"); 2891 } 2892 2893 /** 2894 * Wait until a preview popup containing the given result is shown 2895 * 2896 * @param {*} dbg 2897 * @param {string} result 2898 */ 2899 async function waitForPreviewWithResult(dbg, result) { 2900 info(`Wait for preview popup with result ${result}`); 2901 await waitUntil(async () => { 2902 const previewEl = await waitForElement(dbg, "previewPopup"); 2903 return previewEl.innerText.includes(result); 2904 }); 2905 } 2906 2907 /** 2908 * Expand or collapse a node in the preview popup 2909 * 2910 * @param {*} dbg 2911 * @param {number} index 2912 */ 2913 async function toggleExpanded(dbg, index) { 2914 let initialNodesLength; 2915 await waitFor(() => { 2916 const nodes = findElement(dbg, "previewPopup")?.querySelectorAll(".node"); 2917 if (nodes?.length > index) { 2918 initialNodesLength = nodes.length; 2919 nodes[index].querySelector(".theme-twisty").click(); 2920 return true; 2921 } 2922 return false; 2923 }); 2924 await waitFor( 2925 () => 2926 findElement(dbg, "previewPopup").querySelectorAll(".node").length !== 2927 initialNodesLength 2928 ); 2929 } 2930 2931 async function waitForBreakableLine(dbg, source, lineNumber) { 2932 await waitForState( 2933 dbg, 2934 () => { 2935 const currentSource = findSource(dbg, source); 2936 2937 const breakableLines = 2938 currentSource && dbg.selectors.getBreakableLines(currentSource.id); 2939 2940 return breakableLines && breakableLines.includes(lineNumber); 2941 }, 2942 `waiting for breakable line ${lineNumber}` 2943 ); 2944 } 2945 2946 async function waitForSourceTreeThreadsCount(dbg, i) { 2947 info(`Waiting for ${i} threads in the source tree`); 2948 await waitUntil(() => { 2949 return findAllElements(dbg, "sourceTreeThreads").length === i; 2950 }); 2951 } 2952 2953 function getDisplayedSourceTree(dbg) { 2954 return [...findAllElements(dbg, "sourceNodes")]; 2955 } 2956 2957 function getDisplayedSourceElements(dbg) { 2958 return [...findAllElements(dbg, "sourceTreeFiles")]; 2959 } 2960 2961 function getDisplayedSources(dbg) { 2962 return getDisplayedSourceElements(dbg).map(e => { 2963 // Replace some non visible space characters that prevents Array.includes from working correctly 2964 return e.textContent.trim().replace(/^[\s\u200b]*/g, ""); 2965 }); 2966 } 2967 2968 /** 2969 * Wait for a single source to be visible in the Source Tree. 2970 */ 2971 async function waitForSourceInSourceTree(dbg, fileName) { 2972 return waitFor( 2973 async () => { 2974 await expandSourceTree(dbg); 2975 2976 return getDisplayedSourceElements(dbg).find(e => { 2977 // Replace some non visible space characters that prevents Array.includes from working correctly 2978 return e.textContent.trim().replace(/^[\s\u200b]*/g, "") == fileName; 2979 }); 2980 }, 2981 null, 2982 100, 2983 50 2984 ); 2985 } 2986 2987 /** 2988 * Wait for a precise list of sources to be shown in the Source Tree. 2989 * No more, no less than the list. 2990 */ 2991 async function waitForSourcesInSourceTree( 2992 dbg, 2993 sources, 2994 { noExpand = false } = {} 2995 ) { 2996 info(`waiting for ${sources.length} files in the source tree`); 2997 try { 2998 // Use custom timeout and retry count for waitFor as the test method is slow to resolve 2999 // and default value makes the timeout unecessarily long 3000 await waitFor( 3001 async () => { 3002 if (!noExpand) { 3003 await expandSourceTree(dbg); 3004 } 3005 const displayedSources = getDisplayedSources(dbg); 3006 return ( 3007 displayedSources.length == sources.length && 3008 sources.every(source => displayedSources.includes(source)) 3009 ); 3010 }, 3011 null, 3012 100, 3013 50 3014 ); 3015 } catch (e) { 3016 // Craft a custom error message to help understand what's wrong with the Source Tree content 3017 const displayedSources = getDisplayedSources(dbg); 3018 let msg = "Invalid Source Tree Content.\n"; 3019 const missingElements = []; 3020 for (const source of sources) { 3021 const idx = displayedSources.indexOf(source); 3022 if (idx != -1) { 3023 displayedSources.splice(idx, 1); 3024 } else { 3025 missingElements.push(source); 3026 } 3027 } 3028 if (missingElements.length) { 3029 msg += "Missing elements: " + missingElements.join(", ") + "\n"; 3030 } 3031 if (displayedSources.length) { 3032 msg += "Unexpected elements: " + displayedSources.join(", "); 3033 } 3034 throw new Error(msg); 3035 } 3036 } 3037 3038 async function waitForNodeToGainFocus(dbg, index) { 3039 info(`Waiting for source node #${index} to be focused`); 3040 await waitUntil(() => { 3041 const element = findElement(dbg, "sourceNode", index); 3042 3043 if (element) { 3044 return element.classList.contains("focused"); 3045 } 3046 3047 return false; 3048 }); 3049 } 3050 3051 async function assertNodeIsFocused(dbg, index) { 3052 await waitForNodeToGainFocus(dbg, index); 3053 const node = findElement(dbg, "sourceNode", index); 3054 ok(node.classList.contains("focused"), `node ${index} is focused`); 3055 } 3056 3057 /** 3058 * Asserts that the debugger is paused and the debugger tab is 3059 * highlighted. 3060 * 3061 * @param {*} toolbox 3062 * @returns 3063 */ 3064 async function assertDebuggerIsHighlightedAndPaused(toolbox) { 3065 info("Wait for the debugger to be automatically selected on pause"); 3066 await waitUntil(() => toolbox.currentToolId == "jsdebugger"); 3067 ok(true, "Debugger selected"); 3068 3069 // Wait for the debugger to finish loading. 3070 await toolbox.getPanelWhenReady("jsdebugger"); 3071 3072 // And to be fully paused 3073 const dbg = createDebuggerContext(toolbox); 3074 await waitForPaused(dbg); 3075 3076 ok(toolbox.isHighlighted("jsdebugger"), "Debugger is highlighted"); 3077 3078 return dbg; 3079 } 3080 3081 async function addExpression(dbg, input) { 3082 info("Adding an expression"); 3083 3084 const plusIcon = findElement(dbg, "expressionPlus"); 3085 if (plusIcon) { 3086 plusIcon.click(); 3087 } 3088 findElement(dbg, "expressionInput").focus(); 3089 type(dbg, input); 3090 const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSION"); 3091 const clearAutocomplete = waitForDispatch(dbg.store, "CLEAR_AUTOCOMPLETE"); 3092 pressKey(dbg, "Enter"); 3093 await evaluated; 3094 await clearAutocomplete; 3095 } 3096 3097 async function editExpression(dbg, input) { 3098 info("Updating the expression"); 3099 dblClickElement(dbg, "expressionNode", 1); 3100 // Position cursor reliably at the end of the text. 3101 pressKey(dbg, "End"); 3102 type(dbg, input); 3103 const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSIONS"); 3104 pressKey(dbg, "Enter"); 3105 await evaluated; 3106 } 3107 3108 /** 3109 * Get the text representation of a watch expression label given its position in the panel 3110 * 3111 * @param {object} dbg 3112 * @param {number} index: Position in the panel of the expression we want the label of 3113 * @returns {string} 3114 */ 3115 function getWatchExpressionLabel(dbg, index) { 3116 return findElement(dbg, "expressionNode", index).innerText; 3117 } 3118 3119 /** 3120 * Get the text representation of a watch expression value given its position in the panel 3121 * 3122 * @param {object} dbg 3123 * @param {number} index: Position in the panel of the expression we want the value of 3124 * @returns {string} 3125 */ 3126 function getWatchExpressionValue(dbg, index) { 3127 return findElement(dbg, "expressionValue", index).innerText; 3128 } 3129 3130 // Return a promise with a reference to jsterm, opening the split 3131 // console if necessary. This cleans up the split console pref so 3132 // it won't pollute other tests. 3133 async function getDebuggerSplitConsole(dbg) { 3134 let { toolbox, win } = dbg; 3135 3136 if (!win) { 3137 win = toolbox.win; 3138 } 3139 3140 if (!toolbox.splitConsole) { 3141 pressKey(dbg, "Escape"); 3142 } 3143 3144 await toolbox.openSplitConsole(); 3145 return toolbox.getPanel("webconsole"); 3146 } 3147 3148 // Return a promise that resolves with the result of a thread evaluating a 3149 // string in the topmost frame. 3150 async function evaluateInTopFrame(dbg, text) { 3151 const threadFront = dbg.toolbox.target.threadFront; 3152 const { frames } = await threadFront.getFrames(0, 1); 3153 ok(frames.length == 1, "Got one frame"); 3154 const response = await dbg.commands.scriptCommand.execute(text, { 3155 frameActor: frames[0].actorID, 3156 }); 3157 return response.result.type == "undefined" ? undefined : response.result; 3158 } 3159 3160 // Return a promise that resolves when a thread evaluates a string in the 3161 // topmost frame, ensuring the result matches the expected value. 3162 async function checkEvaluateInTopFrame(dbg, text, expected) { 3163 const rval = await evaluateInTopFrame(dbg, text); 3164 ok(rval == expected, `Eval returned ${expected}`); 3165 } 3166 3167 async function findConsoleMessage({ toolbox }, query) { 3168 const [message] = await findConsoleMessages(toolbox, query); 3169 const value = message.querySelector(".message-body").innerText; 3170 // There are console messages which might not have a link e.g Error messages 3171 const link = message.querySelector(".frame-link-source")?.innerText; 3172 return { value, link }; 3173 } 3174 3175 async function hasConsoleMessage({ toolbox }, msg) { 3176 return waitFor(async () => { 3177 const messages = await findConsoleMessages(toolbox, msg); 3178 return !!messages.length; 3179 }); 3180 } 3181 3182 function evaluateExpressionInConsole( 3183 hud, 3184 expression, 3185 expectedClassName = "result" 3186 ) { 3187 const seenMessages = new Set( 3188 JSON.parse( 3189 hud.ui.outputNode 3190 .querySelector("[data-visible-messages]") 3191 .getAttribute("data-visible-messages") 3192 ) 3193 ); 3194 const onResult = new Promise(res => { 3195 const onNewMessage = messages => { 3196 for (const message of messages) { 3197 if ( 3198 message.node.classList.contains(expectedClassName) && 3199 !seenMessages.has(message.node.getAttribute("data-message-id")) 3200 ) { 3201 hud.ui.off("new-messages", onNewMessage); 3202 res(message.node); 3203 } 3204 } 3205 }; 3206 hud.ui.on("new-messages", onNewMessage); 3207 }); 3208 hud.ui.wrapper.dispatchEvaluateExpression(expression); 3209 return onResult; 3210 } 3211 3212 function waitForInspectorPanelChange(dbg) { 3213 return dbg.toolbox.getPanelWhenReady("inspector"); 3214 } 3215 3216 function getEagerEvaluationElement(hud) { 3217 return hud.ui.outputNode.querySelector(".eager-evaluation-result"); 3218 } 3219 3220 async function waitForEagerEvaluationResult(hud, text) { 3221 info(`Waiting for eager evaluation result: ${text}`); 3222 await waitUntil(() => { 3223 const elem = getEagerEvaluationElement(hud); 3224 if (elem) { 3225 if (text instanceof RegExp) { 3226 return text.test(elem.innerText); 3227 } 3228 return elem.innerText == text; 3229 } 3230 return false; 3231 }); 3232 ok(true, `Got eager evaluation result ${text}`); 3233 } 3234 3235 function setInputValue(hud, value) { 3236 const onValueSet = hud.jsterm.once("set-input-value"); 3237 hud.jsterm._setValue(value); 3238 return onValueSet; 3239 } 3240 3241 function assertMenuItemChecked(menuItem, isChecked) { 3242 is( 3243 !!menuItem.getAttribute("aria-checked"), 3244 isChecked, 3245 `Item has expected state: ${isChecked ? "checked" : "unchecked"}` 3246 ); 3247 } 3248 3249 async function toggleDebuggerSettingsMenuItem(dbg, { className, isChecked }) { 3250 const menuButton = findElementWithSelector( 3251 dbg, 3252 ".command-bar .debugger-settings-menu-button" 3253 ); 3254 const { parent } = dbg.panel.panelWin; 3255 const { document } = parent; 3256 3257 menuButton.click(); 3258 // Waits for the debugger settings panel to appear. 3259 await waitFor(() => { 3260 const menuListEl = document.querySelector("#debugger-settings-menu-list"); 3261 // Lets check the offsetParent property to make sure the menu list is actually visible 3262 // by its parents display property being no longer "none". 3263 return menuListEl && menuListEl.offsetParent !== null; 3264 }); 3265 3266 const menuItem = document.querySelector(className); 3267 3268 assertMenuItemChecked(menuItem, isChecked); 3269 3270 menuItem.click(); 3271 3272 // Waits for the debugger settings panel to disappear. 3273 await waitFor(() => menuButton.getAttribute("aria-expanded") === "false"); 3274 } 3275 3276 async function toggleSourcesTreeSettingsMenuItem( 3277 dbg, 3278 { className, isChecked } 3279 ) { 3280 const menuButton = findElementWithSelector( 3281 dbg, 3282 ".sources-list .debugger-settings-menu-button" 3283 ); 3284 const { parent } = dbg.panel.panelWin; 3285 const { document } = parent; 3286 3287 menuButton.click(); 3288 // Waits for the debugger settings panel to appear. 3289 await waitFor(() => { 3290 const menuListEl = document.querySelector( 3291 "#sources-tree-settings-menu-list" 3292 ); 3293 // Lets check the offsetParent property to make sure the menu list is actually visible 3294 // by its parents display property being no longer "none". 3295 return menuListEl && menuListEl.offsetParent !== null; 3296 }); 3297 3298 const menuItem = document.querySelector(className); 3299 3300 assertMenuItemChecked(menuItem, isChecked); 3301 3302 menuItem.click(); 3303 3304 // Waits for the debugger settings panel to disappear. 3305 await waitFor(() => menuButton.getAttribute("aria-expanded") === "false"); 3306 } 3307 3308 /** 3309 * Click on the source map button in the editor's footer 3310 * and wait for its context menu to be rendered before clicking 3311 * on one menuitem of it. 3312 * 3313 * @param {object} dbg 3314 * @param {string} className 3315 * The class name of the menuitem to click in the context menu. 3316 */ 3317 async function clickOnSourceMapMenuItem(dbg, className) { 3318 const menuButton = findElement(dbg, "sourceMapFooterButton"); 3319 const { parent } = dbg.panel.panelWin; 3320 const { document } = parent; 3321 3322 menuButton.click(); 3323 // Waits for the debugger settings panel to appear. 3324 await waitFor(() => { 3325 const menuListEl = document.querySelector("#debugger-source-map-list"); 3326 // Lets check the offsetParent property to make sure the menu list is actually visible 3327 // by its parents display property being no longer "none". 3328 return menuListEl && menuListEl.offsetParent !== null; 3329 }); 3330 3331 const menuItem = document.querySelector(className); 3332 menuItem.click(); 3333 } 3334 3335 async function setLogPoint(dbg, index, value, showStacktrace = false) { 3336 // Wait a bit for CM6 to complete any updates so the log panel 3337 // does not lose focus after the it has been opened 3338 await waitForDocumentLoadComplete(dbg); 3339 rightClickElement(dbg, "gutterElement", index); 3340 await waitForContextMenu(dbg); 3341 3342 selectDebuggerContextMenuItem( 3343 dbg, 3344 `${selectors.addLogItem},${selectors.editLogItem}` 3345 ); 3346 await waitForConditionalPanelFocus(dbg); 3347 3348 const { document } = dbg.win; 3349 3350 if (showStacktrace) { 3351 const checkbox = document.querySelector("#showStacktrace"); 3352 checkbox.click(); 3353 ok(checkbox.checked, "Stacktrace checkbox is checked"); 3354 } 3355 3356 if (value) { 3357 const onBreakpointSet = waitForDispatch(dbg.store, "SET_BREAKPOINT"); 3358 await typeInPanel(dbg, value, true); 3359 info("Wait for breakpoint set"); 3360 await onBreakpointSet; 3361 ok(true, "breakpoint set"); 3362 } 3363 } 3364 3365 /** 3366 * Opens the project search panel 3367 * 3368 * @param {object} dbg 3369 * @return {boolean} The project search is open 3370 */ 3371 function openProjectSearch(dbg) { 3372 info("Opening the project search panel"); 3373 synthesizeKeyShortcut("CmdOrCtrl+Shift+F"); 3374 return waitForState(dbg, () => dbg.selectors.getActiveSearch() === "project"); 3375 } 3376 3377 /** 3378 * Starts a project search based on the specified search term 3379 * 3380 * @param {object} dbg 3381 * @param {string} searchTerm - The test to search for 3382 * @param {number} expectedResults - The expected no of results to wait for. 3383 * This is the number of file results and not the numer of matches in all files. 3384 * When falsy value is passed, expects no match. 3385 * @return {Array} List of search results element nodes 3386 */ 3387 async function doProjectSearch(dbg, searchTerm, expectedResults) { 3388 await clearElement(dbg, "projectSearchSearchInput"); 3389 type(dbg, searchTerm); 3390 pressKey(dbg, "Enter"); 3391 return waitForSearchResults(dbg, expectedResults); 3392 } 3393 3394 /** 3395 * Waits for the search results node to render 3396 * 3397 * @param {object} dbg 3398 * @param {number} expectedResults - The expected no of results to wait for 3399 * This is the number of file results and not the numer of matches in all files. 3400 * @return (Array) List of search result element nodes 3401 */ 3402 async function waitForSearchResults(dbg, expectedResults) { 3403 if (expectedResults) { 3404 info(`Waiting for ${expectedResults} project search results`); 3405 await waitUntil( 3406 () => 3407 findAllElements(dbg, "projectSearchFileResults").length == 3408 expectedResults 3409 ); 3410 } else { 3411 // If no results are expected, wait for the "no results" message to be displayed. 3412 info("Wait for project search to complete with no results"); 3413 await waitUntil(() => { 3414 const projectSearchResult = findElementWithSelector( 3415 dbg, 3416 ".no-result-msg" 3417 ); 3418 return projectSearchResult 3419 ? projectSearchResult.textContent == 3420 DEBUGGER_L10N.getStr("projectTextSearch.noResults") 3421 : false; 3422 }); 3423 } 3424 return findAllElements(dbg, "projectSearchFileResults"); 3425 } 3426 3427 /** 3428 * Get the no of expanded search results 3429 * 3430 * @param {object} dbg 3431 * @return {number} No of expanded results 3432 */ 3433 function getExpandedResultsCount(dbg) { 3434 return findAllElements(dbg, "projectSearchExpandedResults").length; 3435 } 3436 3437 // This module is also loaded for Browser Toolbox tests, within the browser toolbox process 3438 // which doesn't contain mochitests resource://testing-common URL. 3439 // This isn't important to allow rejections in the context of the browser toolbox tests. 3440 const protocolHandler = Services.io 3441 .getProtocolHandler("resource") 3442 .QueryInterface(Ci.nsIResProtocolHandler); 3443 if (protocolHandler.hasSubstitution("testing-common")) { 3444 const { PromiseTestUtils } = ChromeUtils.importESModule( 3445 "resource://testing-common/PromiseTestUtils.sys.mjs" 3446 ); 3447 PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/); 3448 this.PromiseTestUtils = PromiseTestUtils; 3449 3450 // Debugger operations that are canceled because they were rendered obsolete by 3451 // a navigation or pause/resume end up as uncaught rejections. These never 3452 // indicate errors and are allowed in all debugger tests. 3453 // All the following are related to context middleware throwing on obsolete async actions: 3454 PromiseTestUtils.allowMatchingRejectionsGlobally(/DebuggerContextError/); 3455 } 3456 3457 /** 3458 * Selects the specific black box context menu item 3459 * 3460 * @param {object} dbg 3461 * @param {string} itemName 3462 * The name of the context menu item. 3463 */ 3464 async function selectBlackBoxContextMenuItem(dbg, itemName) { 3465 let wait = null; 3466 if (itemName == "blackbox-line" || itemName == "blackbox-lines") { 3467 wait = Promise.any([ 3468 waitForDispatch(dbg.store, "BLACKBOX_SOURCE_RANGES"), 3469 waitForDispatch(dbg.store, "UNBLACKBOX_SOURCE_RANGES"), 3470 ]); 3471 } else if (itemName == "blackbox") { 3472 wait = Promise.any([ 3473 waitForDispatch(dbg.store, "BLACKBOX_WHOLE_SOURCES"), 3474 waitForDispatch(dbg.store, "UNBLACKBOX_WHOLE_SOURCES"), 3475 ]); 3476 } 3477 3478 info(`Select the ${itemName} context menu item`); 3479 selectDebuggerContextMenuItem(dbg, `#node-menu-${itemName}`); 3480 return wait; 3481 } 3482 3483 function openOutlinePanel(dbg, waitForOutlineList = true) { 3484 info("Select the outline panel"); 3485 const outlineTab = findElementWithSelector(dbg, ".outline-tab a"); 3486 EventUtils.synthesizeMouseAtCenter(outlineTab, {}, outlineTab.ownerGlobal); 3487 3488 if (!waitForOutlineList) { 3489 return Promise.resolve(); 3490 } 3491 3492 return waitForElementWithSelector(dbg, ".outline-list"); 3493 } 3494 3495 // Test empty panel when source has not function or class symbols 3496 // Test that anonymous functions do not show in the outline panel 3497 function assertOutlineItems(dbg, expectedItems) { 3498 const outlineItems = Array.from( 3499 findAllElementsWithSelector( 3500 dbg, 3501 ".outline-list h2, .outline-list .outline-list__element" 3502 ) 3503 ); 3504 SimpleTest.isDeeply( 3505 outlineItems.map(i => i.innerText.trim()), 3506 expectedItems, 3507 "The expected items are displayed in the outline panel" 3508 ); 3509 } 3510 3511 async function checkAdditionalThreadCount(dbg, count) { 3512 await waitForState( 3513 dbg, 3514 () => { 3515 return dbg.selectors.getThreads().length == count; 3516 }, 3517 "Have the expected number of additional threads" 3518 ); 3519 ok(true, `Have ${count} threads`); 3520 } 3521 3522 /** 3523 * Retrieve the text displayed as warning under the editor. 3524 */ 3525 function findFooterNotificationMessage(dbg) { 3526 return findElement(dbg, "editorNotificationFooter")?.innerText; 3527 } 3528 3529 /** 3530 * Toggle a JavaScript Tracer settings via the toolbox toolbar button's context menu. 3531 * 3532 * @param {object} dbg 3533 * @param {string} selector 3534 * Selector for the menu item of the settings defined in devtools/client/framework/definitions.js. 3535 */ 3536 async function toggleJsTracerMenuItem(dbg, selector) { 3537 const button = dbg.toolbox.doc.getElementById("command-button-jstracer"); 3538 EventUtils.synthesizeMouseAtCenter( 3539 button, 3540 { type: "contextmenu" }, 3541 dbg.toolbox.win 3542 ); 3543 const popup = await waitForContextMenu(dbg); 3544 const onHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); 3545 selectDebuggerContextMenuItem(dbg, selector); 3546 await onHidden; 3547 } 3548 3549 /** 3550 * Asserts that the number of displayed inline previews, the contents of the inline previews and the lines 3551 * that they are displayed on, are accurate 3552 * 3553 * @param {object} dbg 3554 * @param {Array} expectedInlinePreviews 3555 * @param {string} fnName 3556 */ 3557 async function assertInlinePreviews(dbg, expectedInlinePreviews, fnName) { 3558 // Accumulate all the previews over the various lines 3559 let expectedNumberOfInlinePreviews = 0; 3560 for (const { previews } of expectedInlinePreviews) { 3561 expectedNumberOfInlinePreviews += previews.length; 3562 } 3563 3564 const inlinePreviews = await waitForAllElements( 3565 dbg, 3566 "visibleInlinePreviews", 3567 expectedNumberOfInlinePreviews, 3568 true 3569 ); 3570 3571 ok(true, `Displayed ${inlinePreviews.length} inline previews`); 3572 3573 for (const expectedInlinePreview of expectedInlinePreviews) { 3574 const { previews, line } = expectedInlinePreview; 3575 3576 const inlinePreviewElsOnLine = findAllElements( 3577 dbg, 3578 "inlinePreviewsOnLine", 3579 line 3580 ); 3581 previews.forEach(({ identifier, value }, index) => { 3582 const inlinePreviewEl = inlinePreviewElsOnLine[index]; 3583 3584 const actualIdentifier = inlinePreviewEl.querySelector( 3585 ".inline-preview-label" 3586 ).innerText; 3587 is( 3588 inlinePreviewEl.querySelector(".inline-preview-label").innerText, 3589 identifier, 3590 `${identifier} in "${fnName}" has correct inline preview label "${actualIdentifier}" on line "${line}"` 3591 ); 3592 3593 const actualValue = inlinePreviewEl.querySelector( 3594 ".inline-preview-value" 3595 ).innerText; 3596 is( 3597 inlinePreviewEl.querySelector(".inline-preview-value").innerText, 3598 value, 3599 `${identifier} in "${fnName}" has correct inline preview value "${actualValue}" on line "${line}"` 3600 ); 3601 }); 3602 } 3603 }