helper_events_test_runner.js (9073B)
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 /* import-globals-from head.js */ 6 /* import-globals-from helper_diff.js */ 7 "use strict"; 8 9 const beautify = require("resource://devtools/shared/jsbeautify/beautify.js"); 10 11 loadHelperScript("helper_diff.js"); 12 13 /** 14 * Generator function that runs checkEventsForNode() for each object in the 15 * TEST_DATA array. 16 */ 17 async function runEventPopupTests(url, tests) { 18 const { inspector } = await openInspectorForURL(url); 19 20 await inspector.markup.expandAll(); 21 22 for (const test of tests) { 23 await checkEventsForNode(test, inspector); 24 } 25 26 // Wait for promises to avoid leaks when running this as a single test. 27 // We need to do this because we have opened a bunch of popups and don't them 28 // to affect other test runs when they are GCd. 29 await promiseNextTick(); 30 } 31 32 /** 33 * Generator function that takes a selector and expected results and returns 34 * the event info. 35 * 36 * @param {object} test 37 * A test object should contain the following properties: 38 * - selector {String} a css selector targeting the node to edit 39 * - expected {Array} array of expected event objects 40 * - type {String} event type 41 * - filename {String} filename:line where the evt handler is defined 42 * - attributes {Array} array of event attributes ({String}) 43 * - handler {String} string representation of the handler 44 * - beforeTest {Function} (optional) a function to execute on the page 45 * before running the test 46 * - isSourceMapped {Boolean} (optional) true if the location 47 * is source-mapped, requiring some extra delay before the checks 48 * @param {InspectorPanel} inspector The instance of InspectorPanel currently 49 * opened 50 */ 51 async function checkEventsForNode(test, inspector) { 52 const { selector, expected, beforeTest, isSourceMapped } = test; 53 const container = await getContainerForSelector(selector, inspector); 54 55 if (typeof beforeTest === "function") { 56 await beforeTest(inspector); 57 } 58 59 const evHolder = container.elt.querySelector( 60 ".inspector-badge.interactive[data-event]" 61 ); 62 63 if (expected.length === 0) { 64 // If no event is expected, check that event bubble is hidden. 65 ok(!evHolder, "event bubble should be hidden"); 66 return; 67 } 68 69 const tooltip = inspector.markup.eventDetailsTooltip; 70 71 await selectNode(selector, inspector); 72 73 let sourceMapPromise = null; 74 if (isSourceMapped) { 75 sourceMapPromise = tooltip.once("event-tooltip-source-map-ready"); 76 } 77 78 // Click button to show tooltip 79 info("Clicking evHolder"); 80 evHolder.scrollIntoView({ 81 block: "center", 82 inline: "end", 83 behavior: "instant", 84 }); 85 EventUtils.synthesizeMouseAtCenter( 86 evHolder, 87 {}, 88 inspector.markup.doc.defaultView 89 ); 90 await tooltip.once("shown"); 91 info("tooltip shown"); 92 93 if (isSourceMapped) { 94 info("Waiting for source map to be applied"); 95 await sourceMapPromise; 96 } 97 98 // Check values 99 const headers = tooltip.panel.querySelectorAll(".event-header"); 100 const nodeFront = container.node; 101 const cssSelector = nodeFront.nodeName + "#" + nodeFront.id; 102 103 for (let i = 0; i < headers.length; i++) { 104 const label = `${cssSelector}.${expected[i].type} (index ${i})`; 105 info(`${label} START`); 106 107 const header = headers[i]; 108 const type = header.querySelector(".event-tooltip-event-type"); 109 const filename = header.querySelector(".event-tooltip-filename"); 110 const attributes = Array.from( 111 header.querySelectorAll(".event-tooltip-attributes") 112 ); 113 const contentBox = header.nextElementSibling; 114 115 info("Looking for " + type.textContent); 116 117 is(type.textContent, expected[i].type, "type matches for " + cssSelector); 118 is( 119 filename.textContent, 120 expected[i].filename, 121 "filename matches for " + cssSelector 122 ); 123 124 Assert.deepEqual( 125 attributes.map(el => el.textContent), 126 expected[i].attributes, 127 `we have the expected attributes for "${cssSelector}"` 128 ); 129 130 is( 131 header.classList.contains("content-expanded"), 132 false, 133 "We are not in expanded state" 134 ); 135 136 // Make sure the header is not hidden by scrollbars before clicking. 137 header.scrollIntoView(); 138 139 // Avoid clicking the header's center (could hit the debugger button) 140 EventUtils.synthesizeMouse(header, 2, 2, {}, type.ownerGlobal); 141 await tooltip.once("event-tooltip-ready"); 142 143 is( 144 header.classList.contains("content-expanded") && 145 contentBox.hasAttribute("open"), 146 true, 147 "We are in expanded state and icon changed" 148 ); 149 150 is( 151 tooltip.panel.querySelectorAll(".event-header.content-expanded") 152 .length === 1 && 153 tooltip.panel.querySelectorAll(".event-tooltip-content-box[open]") 154 .length === 1, 155 true, 156 "Only one event box is expanded at a time" 157 ); 158 159 const editor = tooltip.eventTooltip._eventEditors.get(contentBox).editor; 160 const tidiedHandler = beautify.js(expected[i].handler, { 161 indent_size: 2, 162 }); 163 testDiff( 164 editor.getText(), 165 tidiedHandler, 166 "handler matches for " + cssSelector, 167 ok 168 ); 169 170 const checkbox = header.querySelector("input[type=checkbox]"); 171 ok(checkbox, "The event toggling checkbox is displayed"); 172 const disabled = checkbox.hasAttribute("disabled"); 173 // We can't disable React/jQuery events at the moment, so ensure that for those, 174 // the checkbox is disabled. 175 const shouldBeDisabled = 176 expected[i].attributes?.includes("React") || 177 expected[i].attributes?.includes("jQuery"); 178 Assert.strictEqual( 179 disabled, 180 shouldBeDisabled, 181 `The checkbox is ${shouldBeDisabled ? "disabled" : "enabled"}\n` 182 ); 183 184 info(`${label} END`); 185 } 186 187 const tooltipHidden = tooltip.once("hidden"); 188 tooltip.hide(); 189 await tooltipHidden; 190 } 191 192 /** 193 * This should be kept in sync with the content of the window load event listener callback 194 * content in doc_markup_events_jquery.html. 195 */ 196 function getDocMarkupEventsJQueryLoadHandlerText() { 197 return ` 198 () => { 199 const handler1 = function liveDivDblClick() { 200 alert(1); 201 }; 202 const handler2 = function liveDivDragStart() { 203 alert(2); 204 }; 205 const handler3 = function liveDivDragLeave() { 206 alert(3); 207 }; 208 const handler4 = function liveDivDragEnd() { 209 alert(4); 210 }; 211 const handler5 = function liveDivDrop() { 212 alert(5); 213 }; 214 const handler6 = function liveDivDragOver() { 215 alert(6); 216 }; 217 const handler7 = function divClick1() { 218 alert(7); 219 }; 220 const handler8 = function divClick2() { 221 alert(8); 222 }; 223 const handler9 = function divKeyDown() { 224 alert(9); 225 }; 226 const handler10 = function divDragOut() { 227 alert(10); 228 }; 229 230 if ($("#livediv").live) { 231 $("#livediv").live("dblclick", handler1); 232 $("#livediv").live("dragstart", handler2); 233 } 234 235 if ($("#livediv").delegate) { 236 $(document).delegate("#livediv", "dragleave", handler3); 237 $(document).delegate("#livediv", "dragend", handler4); 238 } 239 240 if ($("#livediv").on) { 241 $(document).on("drop", "#livediv", handler5); 242 $(document).on("dragover", "#livediv", handler6); 243 $(document).on("dragout", "#livediv:xxxxx", handler10); 244 } 245 246 const div = $("div")[0]; 247 $(div).click(handler7); 248 $(div).click(handler8); 249 $(div).keydown(handler9); 250 251 class MyClass { 252 constructor() { 253 $(document).on("click", '#inclassboundeventdiv', this.onClick.bind(this)); 254 } 255 onClick() { alert(11); } 256 } 257 new MyClass(); 258 }`; 259 } 260 261 /** 262 * Create diff of two strings. 263 * 264 * @param {string} text1 265 * String to compare with text2. 266 * @param {string} text2 [description] 267 * String to compare with text1. 268 * @param {string} msg 269 * Message to display on failure. A diff will be displayed after this 270 * message. 271 */ 272 function testDiff(text1, text2, msg) { 273 let out = ""; 274 275 if (text1 === text2) { 276 ok(true, msg); 277 return; 278 } 279 280 const result = textDiff(text1, text2); 281 282 for (const { atom, operation } of result) { 283 switch (operation) { 284 case "add": 285 out += "+ " + atom + "\n"; 286 break; 287 case "delete": 288 out += "- " + atom + "\n"; 289 break; 290 case "none": 291 out += " " + atom + "\n"; 292 break; 293 } 294 } 295 296 ok(false, msg + "\nDIFF:\n==========\n" + out + "==========\n"); 297 }