shared-head.js (18250B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 /** 6 * Helper methods for finding messages in the virtualized output of the 7 * webconsole. This file can be safely required from other panel test 8 * files. 9 */ 10 11 "use strict"; 12 13 /* eslint-disable no-unused-vars */ 14 15 // Assume that shared-head is always imported before this file 16 /* import-globals-from ../../../shared/test/shared-head.js */ 17 18 /** 19 * Find a message with given messageId in the output, scrolling through the 20 * output from top to bottom in order to make sure the messages are actually 21 * rendered. 22 * 23 * @param object hud 24 * The web console. 25 * @param messageId 26 * A message ID to look for. This could be baked into the selector, but 27 * is provided as a convenience. 28 * @return {Node} the node corresponding the found message 29 */ 30 async function findMessageVirtualizedById({ hud, messageId }) { 31 if (!messageId) { 32 throw new Error("messageId parameter is required"); 33 } 34 35 const elements = await findMessagesVirtualized({ 36 hud, 37 expectedCount: 1, 38 messageId, 39 }); 40 return elements.at(-1); 41 } 42 43 /** 44 * Find the last message with given message type in the output, scrolling 45 * through the output from top to bottom in order to make sure the messages are 46 * actually rendered. 47 * 48 * @param object hud 49 * The web console. 50 * @param string text 51 * A substring that can be found in the message. 52 * @param string typeSelector 53 * A part of selector for the message, to specify the message type. 54 * @return {Node} the node corresponding the found message 55 */ 56 async function findMessageVirtualizedByType({ hud, text, typeSelector }) { 57 const elements = await findMessagesVirtualizedByType({ 58 hud, 59 text, 60 typeSelector, 61 expectedCount: 1, 62 }); 63 return elements.at(-1); 64 } 65 66 /** 67 * Find all messages in the output, scrolling through the output from top 68 * to bottom in order to make sure the messages are actually rendered. 69 * 70 * @param object hud 71 * The web console. 72 * @return {Array} all of the message nodes in the console output. Some of 73 * these may be stale from having been scrolled out of view. 74 */ 75 async function findAllMessagesVirtualized(hud) { 76 return findMessagesVirtualized({ hud }); 77 } 78 79 // This is just a reentrancy guard. Because findMessagesVirtualized mucks 80 // around with the scroll position, if we do something like 81 // let promise1 = findMessagesVirtualized(...); 82 // let promise2 = findMessagesVirtualized(...); 83 // await promise1; 84 // await promise2; 85 // then the two calls will end up messing up each other's expected scroll 86 // position, at which point they could get stuck. This lets us throw an 87 // error when that happens. 88 let gInFindMessagesVirtualized = false; 89 // And this lets us get a little more information in the error - it just holds 90 // the stack of the prior call. 91 let gFindMessagesVirtualizedStack = null; 92 93 /** 94 * Find multiple messages in the output, scrolling through the output from top 95 * to bottom in order to make sure the messages are actually rendered. 96 * 97 * @param object options 98 * @param object options.hud 99 * The web console. 100 * @param options.text [optional] 101 * A substring that can be found in the message. 102 * @param options.typeSelector 103 * A part of selector for the message, to specify the message type. 104 * @param options.expectedCount [optional] 105 * The number of messages to get. This lets us stop scrolling early if 106 * we find that number of messages. 107 * @return {Array} all of the message nodes in the console output matching the 108 * provided filters. If expectedCount is greater than 1, or equal to -1, 109 * some of these may be stale from having been scrolled out of view. 110 */ 111 async function findMessagesVirtualizedByType({ 112 hud, 113 text, 114 typeSelector, 115 expectedCount, 116 }) { 117 if (!typeSelector) { 118 throw new Error("typeSelector parameter is required"); 119 } 120 if (!typeSelector.startsWith(".")) { 121 throw new Error("typeSelector should start with a dot e.g. `.result`"); 122 } 123 124 return findMessagesVirtualized({ 125 hud, 126 text, 127 selector: ".message" + typeSelector, 128 expectedCount, 129 }); 130 } 131 132 /** 133 * Find multiple messages in the output, scrolling through the output from top 134 * to bottom in order to make sure the messages are actually rendered. 135 * 136 * @param object options 137 * @param object options.hud 138 * The web console. 139 * @param options.text [optional] 140 * A substring that can be found in the message. 141 * @param options.selector [optional] 142 * The selector to use in finding the message. 143 * @param options.expectedCount [optional] 144 * The number of messages to get. This lets us stop scrolling early if 145 * we find that number of messages. 146 * @param options.messageId [optional] 147 * A message ID to look for. This could be baked into the selector, but 148 * is provided as a convenience. 149 * @return {Array} all of the message nodes in the console output matching the 150 * provided filters. If expectedCount is greater than 1, or equal to -1, 151 * some of these may be stale from having been scrolled out of view. 152 */ 153 async function findMessagesVirtualized({ 154 hud, 155 text, 156 selector, 157 expectedCount, 158 messageId, 159 }) { 160 if (text === undefined) { 161 text = ""; 162 } 163 if (selector === undefined) { 164 selector = ".message"; 165 } 166 if (expectedCount === undefined) { 167 expectedCount = -1; 168 } 169 170 const outputNode = hud.ui.outputNode; 171 const scrollport = outputNode.querySelector(".webconsole-output"); 172 173 function getVisibleMessageIds() { 174 return JSON.parse(scrollport.getAttribute("data-visible-messages")); 175 } 176 177 function getVisibleMessageMap() { 178 return new Map( 179 JSON.parse(scrollport.getAttribute("data-visible-messages")).map( 180 (id, i) => [id, i] 181 ) 182 ); 183 } 184 185 function getMessageIndex(message) { 186 return getVisibleMessageIds().indexOf( 187 message.getAttribute("data-message-id") 188 ); 189 } 190 191 function getNextMessageId(prevMessage) { 192 const visible = getVisibleMessageIds(); 193 let index = 0; 194 if (prevMessage) { 195 const lastId = prevMessage.getAttribute("data-message-id"); 196 index = visible.indexOf(lastId); 197 if (index === -1) { 198 throw new Error( 199 `Tried to get next message ID for message that doesn't exist. Last seen ID: ${lastId}, all visible ids: [${visible.join( 200 ", " 201 )}]` 202 ); 203 } 204 } 205 if (index + 1 >= visible.length) { 206 return null; 207 } 208 return visible[index + 1]; 209 } 210 211 if (gInFindMessagesVirtualized) { 212 throw new Error( 213 `findMessagesVirtualized was re-entered somehow. This is not allowed. Other stack: [${gFindMessagesVirtualizedStack}]` 214 ); 215 } 216 try { 217 gInFindMessagesVirtualized = true; 218 gFindMessagesVirtualizedStack = new Error().stack; 219 // The console output will automatically scroll to the bottom of the 220 // scrollport in certain circumstances. Because we need to scroll the 221 // output to find all messages, we need to disable this. This attribute 222 // controls that. 223 scrollport.setAttribute("disable-autoscroll", ""); 224 225 // This array is here purely for debugging purposes. We collect the indices 226 // of every element we see in order to validate that we don't have any gaps 227 // in the list. 228 const allIndices = []; 229 230 const allElements = []; 231 const seenIds = new Set(); 232 let lastItem = null; 233 while (true) { 234 if (scrollport.scrollHeight > scrollport.clientHeight) { 235 if (!lastItem && scrollport.scrollTop != 0) { 236 // For simplicity's sake, we always start from the top of the output. 237 scrollport.scrollTop = 0; 238 } else if (!lastItem && scrollport.scrollTop == 0) { 239 // We want to make sure that we actually change the scroll position 240 // here, because we're going to wait for an update below regardless, 241 // just to flush out any changes that may have just happened. If we 242 // don't do this, and there were no changes before this function was 243 // called, then we'll just hang on the promise below. 244 scrollport.scrollTop = 1; 245 } else { 246 // This is the core of the loop. Scroll down to the bottom of the 247 // current scrollport, wait until we see the element after the last 248 // one we've seen, and then harvest the messages that are displayed. 249 scrollport.scrollTop = scrollport.scrollTop + scrollport.clientHeight; 250 } 251 252 // Wait for something to happen in the output before checking for our 253 // expected next message. 254 await new Promise(resolve => 255 hud.ui.once("lazy-message-list-updated-or-noop", resolve) 256 ); 257 258 try { 259 await waitFor(async () => { 260 const nextMessageId = getNextMessageId(lastItem); 261 if ( 262 nextMessageId === undefined || 263 scrollport.querySelector(`[data-message-id="${nextMessageId}"]`) 264 ) { 265 return true; 266 } 267 268 // After a scroll, we typically expect to get an updated list of 269 // elements. However, we have some slack at the top of the list, 270 // because we draw elements above and below the actual scrollport to 271 // avoid white flashes when async scrolling. 272 const scrollTarget = scrollport.scrollTop + scrollport.clientHeight; 273 scrollport.scrollTop = scrollTarget; 274 await new Promise(resolve => 275 hud.ui.once("lazy-message-list-updated-or-noop", resolve) 276 ); 277 return false; 278 }); 279 } catch (e) { 280 throw new Error( 281 `Failed waiting for next message ID (${getNextMessageId( 282 lastItem 283 )}) Visible messages: [${[ 284 ...scrollport.querySelectorAll(".message"), 285 ].map(el => el.getAttribute("data-message-id"))}]` 286 ); 287 } 288 } 289 290 const bottomPlaceholder = scrollport.querySelector( 291 ".lazy-message-list-bottom" 292 ); 293 if (!bottomPlaceholder) { 294 // When there are no messages in the output, there is also no 295 // top/bottom placeholder. There's nothing more to do at this point, 296 // so break and return. 297 break; 298 } 299 300 lastItem = bottomPlaceholder.previousSibling; 301 302 // This chunk is just validating that we have no gaps in our output so 303 // far. 304 const indices = [...scrollport.querySelectorAll("[data-message-id]")] 305 .filter( 306 el => el !== scrollport.firstChild && el !== scrollport.lastChild 307 ) 308 .map(el => getMessageIndex(el)); 309 allIndices.push(...indices); 310 allIndices.sort((lhs, rhs) => lhs - rhs); 311 for (let i = 1; i < allIndices.length; i++) { 312 if ( 313 allIndices[i] != allIndices[i - 1] && 314 allIndices[i] != allIndices[i - 1] + 1 315 ) { 316 throw new Error( 317 `Gap detected in virtualized webconsole output between ${ 318 allIndices[i - 1] 319 } and ${allIndices[i]}. Indices: ${allIndices.join(",")}` 320 ); 321 } 322 } 323 324 const messages = scrollport.querySelectorAll(selector); 325 const filtered = [...messages].filter( 326 el => 327 // Core user filters: 328 el.textContent.includes(text) && 329 (!messageId || el.getAttribute("data-message-id") === messageId) && 330 // Make sure we don't collect duplicate messages: 331 !seenIds.has(el.getAttribute("data-message-id")) 332 ); 333 allElements.push(...filtered); 334 for (const message of filtered) { 335 seenIds.add(message.getAttribute("data-message-id")); 336 } 337 338 if (expectedCount >= 0 && allElements.length >= expectedCount) { 339 break; 340 } 341 342 // If the bottom placeholder has 0 height, it means we've scrolled to the 343 // bottom and output all the items. 344 if (bottomPlaceholder.getBoundingClientRect().height == 0) { 345 break; 346 } 347 348 await waitForTime(0); 349 } 350 351 // Finally, we get the map of message IDs to indices within the output, and 352 // sort the message nodes according to that index. They can come in out of 353 // order for a number of reasons (we continue rendering any messages that 354 // have been expanded, and we always render the topmost and bottommost 355 // messages for a11y reasons.) 356 const idsToIndices = getVisibleMessageMap(); 357 allElements.sort( 358 (lhs, rhs) => 359 idsToIndices.get(lhs.getAttribute("data-message-id")) - 360 idsToIndices.get(rhs.getAttribute("data-message-id")) 361 ); 362 return allElements; 363 } finally { 364 scrollport.removeAttribute("disable-autoscroll"); 365 gInFindMessagesVirtualized = false; 366 gFindMessagesVirtualizedStack = null; 367 } 368 } 369 370 /** 371 * Find the last message with given message type in the output. 372 * 373 * @param object hud 374 * The web console. 375 * @param string text 376 * A substring that can be found in the message. 377 * @param string typeSelector 378 * A part of selector for the message, to specify the message type. 379 * @return {Node} the node corresponding the found message, otherwise undefined 380 */ 381 function findMessageByType(hud, text, typeSelector) { 382 const elements = findMessagesByType(hud, text, typeSelector); 383 return elements.at(-1); 384 } 385 386 /** 387 * Find multiple messages with given message type in the output. 388 * 389 * @param object hud 390 * The web console. 391 * @param string text 392 * A substring that can be found in the message. 393 * @param string typeSelector 394 * A part of selector for the message, to specify the message type. 395 * @return {Array} The nodes corresponding the found messages 396 */ 397 function findMessagesByType(hud, text, typeSelector) { 398 if (!typeSelector) { 399 throw new Error("typeSelector parameter is required"); 400 } 401 if (!typeSelector.startsWith(".")) { 402 throw new Error("typeSelector should start with a dot e.g. `.result`"); 403 } 404 405 const selector = ".message" + typeSelector; 406 const messages = hud.ui.outputNode.querySelectorAll(selector); 407 const elements = Array.from(messages).filter(el => 408 el.textContent.includes(text) 409 ); 410 return elements; 411 } 412 413 /** 414 * Find all messages in the output. 415 * 416 * @param object hud 417 * The web console. 418 * @return {Array} The nodes corresponding the found messages 419 */ 420 function findAllMessages(hud) { 421 const messages = hud.ui.outputNode.querySelectorAll(".message"); 422 return Array.from(messages); 423 } 424 425 /** 426 * Find a part of the last message with given message type in the output. 427 * 428 * @param object hud 429 * The web console. 430 * @param object options 431 * - text : {String} A substring that can be found in the message. 432 * - typeSelector: {String} A part of selector for the message, 433 * to specify the message type. 434 * - partSelector: {String} A selector for the part of the message. 435 * @return {Node} the node corresponding the found part, otherwise undefined 436 */ 437 function findMessagePartByType(hud, options) { 438 const elements = findMessagePartsByType(hud, options); 439 return elements.at(-1); 440 } 441 442 /** 443 * Find parts of multiple messages with given message type in the output. 444 * 445 * @param object hud 446 * The web console. 447 * @param object options 448 * - text : {String} A substring that can be found in the message. 449 * - typeSelector: {String} A part of selector for the message, 450 * to specify the message type. 451 * - partSelector: {String} A selector for the part of the message. 452 * @return {Array} The nodes corresponding the found parts 453 */ 454 function findMessagePartsByType(hud, { text, typeSelector, partSelector }) { 455 if (!typeSelector) { 456 throw new Error("typeSelector parameter is required"); 457 } 458 if (!typeSelector.startsWith(".")) { 459 throw new Error("typeSelector should start with a dot e.g. `.result`"); 460 } 461 if (!partSelector) { 462 throw new Error("partSelector parameter is required"); 463 } 464 465 const selector = ".message" + typeSelector + " " + partSelector; 466 const parts = hud.ui.outputNode.querySelectorAll(selector); 467 const elements = Array.from(parts).filter(el => 468 el.textContent.includes(text) 469 ); 470 return elements; 471 } 472 473 /** 474 * Type-specific wrappers for findMessageByType and findMessagesByType. 475 * 476 * @param object hud 477 * The web console. 478 * @param string text 479 * A substring that can be found in the message. 480 * @param string extraSelector [optional] 481 * An extra part of selector for the message, in addition to 482 * type-specific selector. 483 * @return {Node|Array} See findMessageByType or findMessagesByType. 484 */ 485 function findEvaluationResultMessage(hud, text, extraSelector = "") { 486 return findMessageByType(hud, text, ".result" + extraSelector); 487 } 488 function findEvaluationResultMessages(hud, text, extraSelector = "") { 489 return findMessagesByType(hud, text, ".result" + extraSelector); 490 } 491 function findErrorMessage(hud, text, extraSelector = "") { 492 return findMessageByType(hud, text, ".error" + extraSelector); 493 } 494 function findErrorMessages(hud, text, extraSelector = "") { 495 return findMessagesByType(hud, text, ".error" + extraSelector); 496 } 497 function findWarningMessage(hud, text, extraSelector = "") { 498 return findMessageByType(hud, text, ".warn" + extraSelector); 499 } 500 function findWarningMessages(hud, text, extraSelector = "") { 501 return findMessagesByType(hud, text, ".warn" + extraSelector); 502 } 503 function findConsoleAPIMessage(hud, text, extraSelector = "") { 504 return findMessageByType(hud, text, ".console-api" + extraSelector); 505 } 506 function findConsoleAPIMessages(hud, text, extraSelector = "") { 507 return findMessagesByType(hud, text, ".console-api" + extraSelector); 508 } 509 function findNetworkMessage(hud, text, extraSelector = "") { 510 return findMessageByType(hud, text, ".network" + extraSelector); 511 } 512 function findNetworkMessages(hud, text, extraSelector = "") { 513 return findMessagesByType(hud, text, ".network" + extraSelector); 514 } 515 function findTracerMessages(hud, text, extraSelector = "") { 516 return findMessagesByType(hud, text, ".jstracer" + extraSelector); 517 }