head.js (9674B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */ 4 5 "use strict"; 6 7 // shared-head.js handles imports, constants, and utility functions 8 Services.scriptloader.loadSubScript( 9 "chrome://mochitests/content/browser/devtools/client/framework/test/head.js", 10 this 11 ); 12 13 const JSON_VIEW_PREF = "devtools.jsonview.enabled"; 14 15 // Enable JSON View for the test 16 Services.prefs.setBoolPref(JSON_VIEW_PREF, true); 17 18 registerCleanupFunction(() => { 19 Services.prefs.clearUserPref(JSON_VIEW_PREF); 20 }); 21 22 // XXX move some API into devtools/shared/test/shared-head.js 23 24 /** 25 * Add a new test tab in the browser and load the given url. 26 * 27 * @param {string} url 28 * The url to be loaded in the new tab. 29 * 30 * @param {object} [optional] 31 * An object with the following optional properties: 32 * - appReadyState: The readyState of the JSON Viewer app that you want to 33 * wait for. Its value can be one of: 34 * - "uninitialized": The converter has started the request. 35 * If JavaScript is disabled, there will be no more readyState changes. 36 * - "loading": RequireJS started loading the scripts for the JSON Viewer. 37 * If the load timeouts, there will be no more readyState changes. 38 * - "interactive": The JSON Viewer app loaded, but possibly not all the JSON 39 * data has been received. 40 * - "complete" (default): The app is fully loaded with all the JSON. 41 * - docReadyState: The standard readyState of the document that you want to 42 * wait for. Its value can be one of: 43 * - "loading": The JSON data has not been completely loaded (but the app might). 44 * - "interactive": All the JSON data has been received. 45 * - "complete" (default): Since there aren't sub-resources like images, 46 * behaves as "interactive". Note the app might not be loaded yet. 47 */ 48 async function addJsonViewTab( 49 url, 50 { appReadyState = "complete", docReadyState = "complete" } = {} 51 ) { 52 info("Adding a new JSON tab with URL: '" + url + "'"); 53 const tabAdded = BrowserTestUtils.waitForNewTab(gBrowser, url); 54 const tabLoaded = addTab(url, { waitForLoad: true }); 55 56 // The `tabAdded` promise resolves when the JSON Viewer starts loading. 57 // This is usually what we want, however, it never resolves for unrecognized 58 // content types that trigger a download. 59 // On the other hand, `tabLoaded` always resolves, but not until the document 60 // is fully loaded, which is too late if `docReadyState !== "complete"`. 61 // Therefore, we race both promises. 62 const tab = await Promise.race([tabAdded, tabLoaded]); 63 const browser = tab.linkedBrowser; 64 65 const rootDir = getRootDirectory(gTestPath); 66 67 // Catch RequireJS errors (usually timeouts) 68 const error = tabLoaded.then(() => 69 SpecialPowers.spawn(browser, [], function () { 70 return new Promise((resolve, reject) => { 71 const { requirejs } = content.wrappedJSObject; 72 if (requirejs) { 73 requirejs.onError = err => { 74 info(err); 75 ok(false, "RequireJS error"); 76 reject(err); 77 }; 78 } 79 }); 80 }) 81 ); 82 83 const data = { rootDir, appReadyState, docReadyState }; 84 await Promise.race([ 85 error, 86 // eslint-disable-next-line no-shadow 87 ContentTask.spawn(browser, data, async function (data) { 88 // Check if there is a JSONView object. 89 const { JSONView } = content.wrappedJSObject; 90 if (!JSONView) { 91 throw new Error("The JSON Viewer did not load."); 92 } 93 94 const docReadyStates = ["loading", "interactive", "complete"]; 95 const docReadyIndex = docReadyStates.indexOf(data.docReadyState); 96 const appReadyStates = ["uninitialized", ...docReadyStates]; 97 const appReadyIndex = appReadyStates.indexOf(data.appReadyState); 98 if (docReadyIndex < 0 || appReadyIndex < 0) { 99 throw new Error("Invalid app or doc readyState parameter."); 100 } 101 102 // Wait until the document readyState suffices. 103 const { document } = content; 104 while (docReadyStates.indexOf(document.readyState) < docReadyIndex) { 105 info( 106 `DocReadyState is "${document.readyState}". Await "${data.docReadyState}"` 107 ); 108 await new Promise(resolve => { 109 document.addEventListener("readystatechange", resolve, { 110 once: true, 111 }); 112 }); 113 } 114 115 // Wait until the app readyState suffices. 116 while (appReadyStates.indexOf(JSONView.readyState) < appReadyIndex) { 117 info( 118 `AppReadyState is "${JSONView.readyState}". Await "${data.appReadyState}"` 119 ); 120 await new Promise(resolve => { 121 content.addEventListener("AppReadyStateChange", resolve, { 122 once: true, 123 }); 124 }); 125 } 126 }), 127 ]); 128 129 return tab; 130 } 131 132 /** 133 * Expanding a node in the JSON tree 134 */ 135 function clickJsonNode(selector) { 136 info("Expanding node: '" + selector + "'"); 137 138 // eslint-disable-next-line no-shadow 139 return ContentTask.spawn(gBrowser.selectedBrowser, selector, selector => { 140 content.document.querySelector(selector).click(); 141 }); 142 } 143 144 /** 145 * Select JSON View tab (in the content). 146 */ 147 function selectJsonViewContentTab(name) { 148 info("Selecting tab: '" + name + "'"); 149 150 // eslint-disable-next-line no-shadow 151 return ContentTask.spawn(gBrowser.selectedBrowser, name, async name => { 152 const tabsSelector = ".tabs-menu .tabs-menu-item"; 153 const targetTabSelector = `${tabsSelector}.${CSS.escape(name)}`; 154 const targetTab = content.document.querySelector(targetTabSelector); 155 const targetTabIndex = Array.prototype.indexOf.call( 156 content.document.querySelectorAll(tabsSelector), 157 targetTab 158 ); 159 const targetTabButton = targetTab.querySelector("a"); 160 await new Promise(resolve => { 161 content.addEventListener( 162 "TabChanged", 163 ({ detail: { index } }) => { 164 is(index, targetTabIndex, "Hm?"); 165 if (index === targetTabIndex) { 166 resolve(); 167 } 168 }, 169 { once: true } 170 ); 171 targetTabButton.click(); 172 }); 173 is( 174 targetTabButton.getAttribute("aria-selected"), 175 "true", 176 "Tab is now selected" 177 ); 178 }); 179 } 180 181 function getElementCount(selector) { 182 info("Get element count: '" + selector + "'"); 183 184 return SpecialPowers.spawn( 185 gBrowser.selectedBrowser, 186 [selector], 187 selectorChild => { 188 return content.document.querySelectorAll(selectorChild).length; 189 } 190 ); 191 } 192 193 function getElementText(selector) { 194 info("Get element text: '" + selector + "'"); 195 196 return SpecialPowers.spawn( 197 gBrowser.selectedBrowser, 198 [selector], 199 selectorChild => { 200 const element = content.document.querySelector(selectorChild); 201 return element ? element.textContent : null; 202 } 203 ); 204 } 205 206 function getElementAttr(selector, attr) { 207 info("Get attribute '" + attr + "' for element '" + selector + "'"); 208 209 return SpecialPowers.spawn( 210 gBrowser.selectedBrowser, 211 [selector, attr], 212 (selectorChild, attrChild) => { 213 const element = content.document.querySelector(selectorChild); 214 return element ? element.getAttribute(attrChild) : null; 215 } 216 ); 217 } 218 219 /** 220 * Return the text of a row given its index, e.g. `key: "value"` 221 * 222 * @param {number} rowIndex 223 * @returns {Promise<string>} 224 */ 225 async function getRowText(rowIndex) { 226 const key = await getElementText( 227 `.jsonPanelBox .treeTable .treeRow:nth-of-type(${rowIndex + 1}) .treeLabelCell` 228 ); 229 const value = await getElementText( 230 `.jsonPanelBox .treeTable .treeRow:nth-of-type(${rowIndex + 1}) .treeValueCell` 231 ); 232 return `${key}: ${value}`; 233 } 234 235 function focusElement(selector) { 236 info("Focus element: '" + selector + "'"); 237 238 return SpecialPowers.spawn( 239 gBrowser.selectedBrowser, 240 [selector], 241 selectorChild => { 242 const element = content.document.querySelector(selectorChild); 243 if (element) { 244 element.focus(); 245 } 246 } 247 ); 248 } 249 250 /** 251 * Send the string aStr to the focused element. 252 * 253 * For now this method only works for ASCII characters and emulates the shift 254 * key state on US keyboard layout. 255 */ 256 function sendString(str, selector) { 257 info("Send string: '" + str + "'"); 258 259 return SpecialPowers.spawn( 260 gBrowser.selectedBrowser, 261 [selector, str], 262 (selectorChild, strChild) => { 263 if (selectorChild) { 264 const element = content.document.querySelector(selectorChild); 265 if (element) { 266 element.focus(); 267 } 268 } 269 270 EventUtils.sendString(strChild, content); 271 } 272 ); 273 } 274 275 function waitForTime(delay) { 276 return new Promise(resolve => setTimeout(resolve, delay)); 277 } 278 279 function waitForFilter() { 280 return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 281 return new Promise(resolve => { 282 const firstRow = content.document.querySelector( 283 ".jsonPanelBox .treeTable .treeRow" 284 ); 285 286 // Check if the filter is already set. 287 if (firstRow.classList.contains("hidden")) { 288 resolve(); 289 return; 290 } 291 292 // Wait till the first row has 'hidden' class set. 293 const observer = new content.MutationObserver(function (mutations) { 294 for (let i = 0; i < mutations.length; i++) { 295 const mutation = mutations[i]; 296 if (mutation.attributeName == "class") { 297 if (firstRow.classList.contains("hidden")) { 298 observer.disconnect(); 299 resolve(); 300 break; 301 } 302 } 303 } 304 }); 305 306 observer.observe(firstRow, { attributes: true }); 307 }); 308 }); 309 } 310 311 function normalizeNewLines(value) { 312 return value.replace("(\r\n|\n)", "\n"); 313 }