head.js (21629B)
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 /* eslint no-unused-vars: [2, {"vars": "local"}] */ 5 6 "use strict"; 7 8 // Import the inspector's head.js first (which itself imports shared-head.js). 9 Services.scriptloader.loadSubScript( 10 "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js", 11 this 12 ); 13 14 var { 15 getInplaceEditorForSpan: inplaceEditor, 16 } = require("resource://devtools/client/shared/inplace-editor.js"); 17 var clipboard = require("resource://devtools/shared/platform/clipboard.js"); 18 19 // If a test times out we want to see the complete log and not just the last few 20 // lines. 21 SimpleTest.requestCompleteLog(); 22 23 // Toggle this pref on to see all DevTools event communication. This is hugely 24 // useful for fixing race conditions. 25 // Services.prefs.setBoolPref("devtools.dump.emit", true); 26 27 /** 28 * Some tests may need to import one or more of the test helper scripts. 29 * A test helper script is simply a js file that contains common test code that 30 * is either not common-enough to be in head.js, or that is located in a 31 * separate directory. 32 * The script will be loaded synchronously and in the test's scope. 33 * 34 * @param {string} filePath The file path, relative to the current directory. 35 * Examples: 36 * - "helper_attributes_test_runner.js" 37 */ 38 function loadHelperScript(filePath) { 39 const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); 40 Services.scriptloader.loadSubScript(testDir + "/" + filePath, this); 41 } 42 43 /** 44 * Get the MarkupContainer object instance that corresponds to the given 45 * NodeFront 46 * 47 * @param {NodeFront} nodeFront 48 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 49 * loaded in the toolbox 50 * @return {MarkupContainer} 51 */ 52 function getContainerForNodeFront(nodeFront, { markup }) { 53 return markup.getContainer(nodeFront); 54 } 55 56 /** 57 * Get the MarkupContainer object instance that corresponds to the given 58 * selector 59 * 60 * @param {string | NodeFront} selector 61 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 62 * loaded in the toolbox 63 * @param {boolean} Set to true in the event that the node shouldn't be found. 64 * @return {MarkupContainer} 65 */ 66 var getContainerForSelector = async function ( 67 selector, 68 inspector, 69 expectFailure = false 70 ) { 71 info("Getting the markup-container for node " + selector); 72 const nodeFront = await getNodeFront(selector, inspector); 73 const container = getContainerForNodeFront(nodeFront, inspector); 74 75 if (expectFailure) { 76 ok(!container, "Shouldn't find markup-container for selector: " + selector); 77 } else { 78 ok(container, "Found markup-container for selector: " + selector); 79 } 80 81 return container; 82 }; 83 84 /** 85 * Retrieve the nodeValue for the firstChild of a provided selector on the content page. 86 * 87 * @param {string} selector 88 * @return {string} the nodeValue of the first 89 */ 90 function getFirstChildNodeValue(selector) { 91 return SpecialPowers.spawn( 92 gBrowser.selectedBrowser, 93 [selector], 94 _selector => { 95 return content.document.querySelector(_selector).firstChild.nodeValue; 96 } 97 ); 98 } 99 100 /** 101 * Using the markupview's _waitForChildren function, wait for all queued 102 * children updates to be handled. 103 * 104 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 105 * loaded in the toolbox 106 * @return a promise that resolves when all queued children updates have been 107 * handled 108 */ 109 function waitForChildrenUpdated({ markup }) { 110 info("Waiting for queued children updates to be handled"); 111 return new Promise(resolve => { 112 markup._waitForChildren().then(() => { 113 executeSoon(resolve); 114 }); 115 }); 116 } 117 118 /** 119 * Simulate a click on the markup-container (a line in the markup-view) 120 * that corresponds to the selector passed. 121 * 122 * @param {string | NodeFront} selector 123 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 124 * loaded in the toolbox 125 * @return {Promise} Resolves when the node has been selected. 126 */ 127 var clickContainer = async function (selector, inspector) { 128 info("Clicking on the markup-container for node " + selector); 129 130 const nodeFront = await getNodeFront(selector, inspector); 131 const container = getContainerForNodeFront(nodeFront, inspector); 132 133 const updated = container.selected 134 ? Promise.resolve() 135 : inspector.once("inspector-updated"); 136 EventUtils.synthesizeMouseAtCenter( 137 container.tagLine, 138 { type: "mousedown" }, 139 inspector.markup.doc.defaultView 140 ); 141 EventUtils.synthesizeMouseAtCenter( 142 container.tagLine, 143 { type: "mouseup" }, 144 inspector.markup.doc.defaultView 145 ); 146 return updated; 147 }; 148 149 /** 150 * Focus a given editable element, enter edit mode, set value, and commit 151 * 152 * @param {DOMNode} field The element that gets editable after receiving focus 153 * and <ENTER> keypress 154 * @param {string} value The string value to be set into the edited field 155 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 156 * loaded in the toolbox 157 */ 158 function setEditableFieldValue(field, value, inspector) { 159 field.focus(); 160 EventUtils.sendKey("return", inspector.panelWin); 161 const input = inplaceEditor(field).input; 162 ok(input, "Found editable field for setting value: " + value); 163 input.value = value; 164 EventUtils.sendKey("return", inspector.panelWin); 165 } 166 167 /** 168 * Focus the new-attribute inplace-editor field of a node's markup container 169 * and enters the given text, then wait for it to be applied and the for the 170 * node to mutates (when new attribute(s) is(are) created) 171 * 172 * @param {string} selector The selector for the node to edit. 173 * @param {string} text The new attribute text to be entered (e.g. "id='test'") 174 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 175 * loaded in the toolbox 176 * @return a promise that resolves when the node has mutated 177 */ 178 var addNewAttributes = async function (selector, text, inspector) { 179 info(`Entering text "${text}" in new attribute field for node ${selector}`); 180 181 const container = await focusNode(selector, inspector); 182 ok(container, "The container for '" + selector + "' was found"); 183 184 info("Listening for the markupmutation event"); 185 const nodeMutated = inspector.once("markupmutation"); 186 setEditableFieldValue(container.editor.newAttr, text, inspector); 187 await nodeMutated; 188 }; 189 190 /** 191 * Checks that a node has the given attributes. 192 * 193 * @param {string} selector The selector for the node to check. 194 * @param {object} expected An object containing the attributes to check. 195 * e.g. {id: "id1", class: "someclass"} 196 * 197 * Note that node.getAttribute() returns attribute values provided by the HTML 198 * parser. The parser only provides unescaped entities so & will return &. 199 */ 200 var assertAttributes = async function (selector, expected) { 201 const actualAttributes = await getContentPageElementAttributes(selector); 202 is( 203 actualAttributes.length, 204 Object.keys(expected).length, 205 "The node " + selector + " has the expected number of attributes." 206 ); 207 for (const attr in expected) { 208 const foundAttr = actualAttributes.find(({ name }) => name === attr); 209 const foundValue = foundAttr ? foundAttr.value : undefined; 210 ok(foundAttr, "The node " + selector + " has the attribute " + attr); 211 is( 212 foundValue, 213 expected[attr], 214 "The node " + selector + " has the correct " + attr + " attribute value" 215 ); 216 } 217 }; 218 219 /** 220 * Undo the last markup-view action and wait for the corresponding mutation to 221 * occur 222 * 223 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 224 * loaded in the toolbox 225 * @return a promise that resolves when the markup-mutation has been treated or 226 * rejects if no undo action is possible 227 */ 228 function undoChange(inspector) { 229 const canUndo = inspector.markup.undo.canUndo(); 230 ok(canUndo, "The last change in the markup-view can be undone"); 231 if (!canUndo) { 232 return Promise.reject(); 233 } 234 235 const mutated = inspector.once("markupmutation"); 236 inspector.markup.undo.undo(); 237 return mutated; 238 } 239 240 /** 241 * Redo the last markup-view action and wait for the corresponding mutation to 242 * occur 243 * 244 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 245 * loaded in the toolbox 246 * @return a promise that resolves when the markup-mutation has been treated or 247 * rejects if no redo action is possible 248 */ 249 function redoChange(inspector) { 250 const canRedo = inspector.markup.undo.canRedo(); 251 ok(canRedo, "The last change in the markup-view can be redone"); 252 if (!canRedo) { 253 return Promise.reject(); 254 } 255 256 const mutated = inspector.once("markupmutation"); 257 inspector.markup.undo.redo(); 258 return mutated; 259 } 260 261 /** 262 * Check to see if the inspector menu items for editing are disabled. 263 * Things like Edit As HTML, Delete Node, etc. 264 * 265 * @param {NodeFront} nodeFront 266 * @param {InspectorPanel} inspector 267 * @param {boolean} assert Should this function run assertions inline. 268 * @return A promise that resolves with a boolean indicating whether 269 * the menu items are disabled once the menu has been checked. 270 */ 271 var isEditingMenuDisabled = async function ( 272 nodeFront, 273 inspector, 274 assert = true 275 ) { 276 // To ensure clipboard contains something to paste. 277 clipboard.copyString("<p>test</p>"); 278 279 await selectNode(nodeFront, inspector); 280 const allMenuItems = openContextMenuAndGetAllItems(inspector); 281 282 const deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete"); 283 const editHTMLMenuItem = allMenuItems.find( 284 i => i.id === "node-menu-edithtml" 285 ); 286 const pasteHTMLMenuItem = allMenuItems.find( 287 i => i.id === "node-menu-pasteouterhtml" 288 ); 289 290 if (assert) { 291 ok(deleteMenuItem.disabled, "Delete menu item is disabled"); 292 ok(editHTMLMenuItem.disabled, "Edit HTML menu item is disabled"); 293 ok(pasteHTMLMenuItem.disabled, "Paste HTML menu item is disabled"); 294 } 295 296 return ( 297 deleteMenuItem.disabled && 298 editHTMLMenuItem.disabled && 299 pasteHTMLMenuItem.disabled 300 ); 301 }; 302 303 /** 304 * Check to see if the inspector menu items for editing are enabled. 305 * Things like Edit As HTML, Delete Node, etc. 306 * 307 * @param {NodeFront} nodeFront 308 * @param {InspectorPanel} inspector 309 * @param {boolean} assert Should this function run assertions inline. 310 * @return A promise that resolves with a boolean indicating whether 311 * the menu items are enabled once the menu has been checked. 312 */ 313 var isEditingMenuEnabled = async function ( 314 nodeFront, 315 inspector, 316 assert = true 317 ) { 318 // To ensure clipboard contains something to paste. 319 clipboard.copyString("<p>test</p>"); 320 321 await selectNode(nodeFront, inspector); 322 const allMenuItems = openContextMenuAndGetAllItems(inspector); 323 324 const deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete"); 325 const editHTMLMenuItem = allMenuItems.find( 326 i => i.id === "node-menu-edithtml" 327 ); 328 const pasteHTMLMenuItem = allMenuItems.find( 329 i => i.id === "node-menu-pasteouterhtml" 330 ); 331 332 if (assert) { 333 ok(!deleteMenuItem.disabled, "Delete menu item is enabled"); 334 ok(!editHTMLMenuItem.disabled, "Edit HTML menu item is enabled"); 335 ok(!pasteHTMLMenuItem.disabled, "Paste HTML menu item is enabled"); 336 } 337 338 return ( 339 !deleteMenuItem.disabled && 340 !editHTMLMenuItem.disabled && 341 !pasteHTMLMenuItem.disabled 342 ); 343 }; 344 345 /** 346 * Wait for all current promises to be resolved. See this as executeSoon that 347 * can be used with yield. 348 */ 349 function promiseNextTick() { 350 return new Promise(resolve => { 351 executeSoon(resolve); 352 }); 353 } 354 355 /** 356 * `await` with timeout. 357 * 358 * Usage: 359 * const badgeEventAdded = inspector.markup.once("badge-added-event"); 360 * ... 361 * const result = await awaitWithTimeout(badgeEventAdded, 3000); 362 * is(result, "timeout", "Ensure that no event badges were added"); 363 * 364 * @param {Promise} promise 365 * Promise to resolve 366 * @param {number} ms 367 * Milliseconds to wait. 368 * @return "timeout" on timeout, otherwise the result of the fulfilled promise. 369 */ 370 async function awaitWithTimeout(promise, ms) { 371 const timeout = new Promise(resolve => { 372 // eslint-disable-next-line 373 const wait = setTimeout(() => { 374 clearTimeout(wait); 375 resolve("timeout"); 376 }, ms); 377 }); 378 379 return Promise.race([promise, timeout]); 380 } 381 382 /** 383 * Collapses the current text selection in an input field and tabs to the next 384 * field. 385 */ 386 function collapseSelectionAndTab(inspector) { 387 // collapse selection and move caret to end 388 EventUtils.sendKey("tab", inspector.panelWin); 389 // next element 390 EventUtils.sendKey("tab", inspector.panelWin); 391 } 392 393 /** 394 * Collapses the current text selection in an input field and tabs to the 395 * previous field. 396 */ 397 function collapseSelectionAndShiftTab(inspector) { 398 // collapse selection and move caret to end 399 EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, inspector.panelWin); 400 // previous element 401 EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, inspector.panelWin); 402 } 403 404 /** 405 * Check that the current focused element is an attribute element in the markup 406 * view. 407 * 408 * @param {string} attrName The attribute name expected to be found 409 * @param {boolean} editMode Whether or not the attribute should be in edit mode 410 */ 411 function checkFocusedAttribute(attrName, editMode) { 412 const focusedAttr = Services.focus.focusedElement; 413 ok(focusedAttr, "Has a focused element"); 414 415 const dataAttr = focusedAttr.parentNode.dataset.attr; 416 is(dataAttr, attrName, attrName + " attribute editor is currently focused."); 417 if (editMode) { 418 // Using a multiline editor for attributes, the focused element should be a textarea. 419 is(focusedAttr.tagName, "textarea", attrName + "is in edit mode"); 420 } else { 421 is(focusedAttr.tagName, "span", attrName + "is not in edit mode"); 422 } 423 } 424 425 /** 426 * Get attributes for node as how they are represented in editor. 427 * 428 * @param {string} selector 429 * @param {InspectorPanel} inspector 430 * @return {Promise} 431 * A promise that resolves with an array of attribute names 432 * (e.g. ["id", "class", "href"]) 433 */ 434 var getAttributesFromEditor = async function (selector, inspector) { 435 const nodeList = ( 436 await getContainerForSelector(selector, inspector) 437 ).tagLine.querySelectorAll("[data-attr]"); 438 439 return [...nodeList].map(node => node.getAttribute("data-attr")); 440 }; 441 442 /** 443 * Simulate dragging a MarkupContainer by calling its mousedown and mousemove 444 * handlers. 445 * 446 * @param {InspectorPanel} inspector The current inspector-panel instance. 447 * @param {string | MarkupContainer} selector The selector to identify the node or 448 * the MarkupContainer for this node. 449 * @param {number} xOffset Optional x offset to drag by. 450 * @param {number} yOffset Optional y offset to drag by. 451 */ 452 async function simulateNodeDrag( 453 inspector, 454 selector, 455 xOffset = 10, 456 yOffset = 10 457 ) { 458 const container = 459 typeof selector === "string" 460 ? await getContainerForSelector(selector, inspector) 461 : selector; 462 container.elt.scrollIntoView(true); 463 const rect = container.tagLine.getBoundingClientRect(); 464 const scrollX = inspector.markup.doc.documentElement.scrollLeft; 465 const scrollY = inspector.markup.doc.documentElement.scrollTop; 466 467 info("Simulate mouseDown on element " + selector); 468 container._onMouseDown({ 469 target: container.tagLine, 470 button: 0, 471 pageX: scrollX + rect.x, 472 pageY: scrollY + rect.y, 473 stopPropagation: () => {}, 474 preventDefault: () => {}, 475 }); 476 477 // _onMouseDown selects the node, so make sure to wait for the 478 // inspector-updated event if the current selection was different. 479 if (inspector.selection.nodeFront !== container.node) { 480 await inspector.once("inspector-updated"); 481 } 482 483 info("Simulate mouseMove on element " + selector); 484 container.onMouseMove({ 485 pageX: scrollX + rect.x + xOffset, 486 pageY: scrollY + rect.y + yOffset, 487 }); 488 } 489 490 /** 491 * Simulate dropping a MarkupContainer by calling its mouseup handler. This is 492 * meant to be called after simulateNodeDrag has been called. 493 * 494 * @param {InspectorPanel} inspector The current inspector-panel instance. 495 * @param {string | MarkupContainer} selector The selector to identify the node or 496 * the MarkupContainer for this node. 497 */ 498 async function simulateNodeDrop(inspector, selector) { 499 info("Simulate mouseUp on element " + selector); 500 const container = 501 typeof selector === "string" 502 ? await getContainerForSelector(selector, inspector) 503 : selector; 504 container.onMouseUp(); 505 inspector.markup._onMouseUp(); 506 } 507 508 /** 509 * Simulate drag'n'dropping a MarkupContainer by calling its mousedown, 510 * mousemove and mouseup handlers. 511 * 512 * @param {InspectorPanel} inspector The current inspector-panel instance. 513 * @param {string | MarkupContainer} selector The selector to identify the node or 514 * the MarkupContainer for this node. 515 * @param {number} xOffset Optional x offset to drag by. 516 * @param {number} yOffset Optional y offset to drag by. 517 */ 518 async function simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) { 519 await simulateNodeDrag(inspector, selector, xOffset, yOffset); 520 await simulateNodeDrop(inspector, selector); 521 } 522 523 /** 524 * Waits until the element has not scrolled for 30 consecutive frames. 525 */ 526 async function waitForScrollStop(doc) { 527 const el = doc.documentElement; 528 const win = doc.defaultView; 529 let lastScrollTop = el.scrollTop; 530 let stopFrameCount = 0; 531 while (stopFrameCount < 30) { 532 // Wait for a frame. 533 await new Promise(resolve => win.requestAnimationFrame(resolve)); 534 535 // Check if the element has scrolled. 536 if (lastScrollTop == el.scrollTop) { 537 // No scrolling since the last frame. 538 stopFrameCount++; 539 } else { 540 // The element has scrolled. Reset the frame counter. 541 stopFrameCount = 0; 542 lastScrollTop = el.scrollTop; 543 } 544 } 545 546 return lastScrollTop; 547 } 548 549 /** 550 * Select a node in the inspector and try to delete it using the provided key. After that, 551 * check that the expected element is focused. 552 * 553 * @param {InspectorPanel} inspector 554 * The current inspector-panel instance. 555 * @param {string} key 556 * The key to simulate to delete the node 557 * @param {object} 558 * - {String} selector: selector of the element to delete. 559 * - {String} focusedSelector: selector of the element that should be selected 560 * after deleting the node. 561 * - {String} pseudo: optional, "before" or "after" if the element focused after 562 * deleting the node is supposed to be a before/after pseudo-element. 563 */ 564 async function checkDeleteAndSelection( 565 inspector, 566 key, 567 { selector, focusedSelector, pseudo } 568 ) { 569 info( 570 "Test deleting node " + 571 selector + 572 " with " + 573 key + 574 ", " + 575 "expecting " + 576 focusedSelector + 577 " to be focused" 578 ); 579 580 info("Select node " + selector + " and make sure it is focused"); 581 await selectNode(selector, inspector); 582 await clickContainer(selector, inspector); 583 584 info("Delete the node with: " + key); 585 const mutated = inspector.once("markupmutation"); 586 EventUtils.sendKey(key, inspector.panelWin); 587 await Promise.all([mutated, inspector.once("inspector-updated")]); 588 589 let nodeFront = await getNodeFront(focusedSelector, inspector); 590 if (pseudo) { 591 // Update the selector for logging in case of failure. 592 focusedSelector = focusedSelector + "::" + pseudo; 593 // Retrieve the :before or :after pseudo element of the nodeFront. 594 const { nodes } = await inspector.walker.children(nodeFront); 595 nodeFront = pseudo === "before" ? nodes[0] : nodes[nodes.length - 1]; 596 } 597 598 is( 599 inspector.selection.nodeFront, 600 nodeFront, 601 focusedSelector + " is selected after deletion" 602 ); 603 604 info("Check that the node was really removed"); 605 let node = await getNodeFront(selector, inspector); 606 ok(!node, "The node can't be found in the page anymore"); 607 608 info("Undo the deletion to restore the original markup"); 609 await undoChange(inspector); 610 node = await getNodeFront(selector, inspector); 611 ok(node, "The node is back"); 612 } 613 614 /** 615 * Click on the reveal link the provided slotted container. 616 * Will resolve when selection emits "new-node-front". 617 */ 618 async function clickOnRevealLink(inspector, container) { 619 const onSelection = inspector.selection.once("new-node-front"); 620 const revealLink = container.elt.querySelector(".reveal-link"); 621 const tagline = revealLink.closest(".tag-line"); 622 const win = inspector.markup.doc.defaultView; 623 624 // First send a mouseover on the tagline to force the link to be displayed. 625 EventUtils.synthesizeMouseAtCenter(tagline, { type: "mouseover" }, win); 626 EventUtils.synthesizeMouseAtCenter(revealLink, {}, win); 627 628 await onSelection; 629 } 630 631 /** 632 * Hit `key` on the reveal link in the provided slotted container. 633 * Will resolve when selection emits "new-node-front". 634 */ 635 async function keydownOnRevealLink(key, inspector, container) { 636 const revealLink = container.elt.querySelector(".reveal-link"); 637 const win = inspector.markup.doc.defaultView; 638 639 const root = inspector.markup.getContainer(inspector.markup._rootNode); 640 root.elt.focus(); 641 642 // we need to go through a ENTER + TAB key sequence to focus on 643 // the .reveal-link element with the keyboard 644 const revealFocused = once(revealLink, "focus"); 645 EventUtils.synthesizeKey("KEY_Enter", {}, win); 646 EventUtils.synthesizeKey("KEY_Tab", {}, win); 647 info("Waiting for .reveal-link to be focused"); 648 await revealFocused; 649 650 // hit `key` on the .reveal-link 651 const onSelection = inspector.selection.once("new-node-front"); 652 EventUtils.synthesizeKey(key, {}, win); 653 await onSelection; 654 }