browser_jsterm_eager_evaluation.js (14420B)
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 "use strict"; 6 7 const TEST_URI = `https://example.com/document-builder.sjs?html=${encodeURIComponent(` 8 <!DOCTYPE html> 9 <script> 10 let x = 3, y = 4; 11 function zzyzx() { 12 x = 10; 13 } 14 function zzyzx2() { 15 x = 10; 16 } 17 var obj = {propA: "A", propB: "B"}; 18 var array = [1, 2, 3]; 19 var $$ = 42; 20 </script> 21 <h1>title</h1> 22 <iframe src="https://example.com/document-builder.sjs?html=in iframe"></iframe> 23 `)}`; 24 25 const EAGER_EVALUATION_PREF = "devtools.webconsole.input.eagerEvaluation"; 26 27 // Basic testing of eager evaluation functionality. Expressions which can be 28 // eagerly evaluated should show their results, and expressions with side 29 // effects should not perform those side effects. 30 add_task(async function () { 31 // Open the inspector first to select a node, so that we can later test "$0" 32 const toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); 33 await selectNodeWithPicker(toolbox, "h1"); 34 35 info("Picker mode stopped, <h1> selected, now switching to the console"); 36 const hud = await openConsole(); 37 38 // Do an evaluation to populate $_ 39 await executeAndWaitForResultMessage( 40 hud, 41 "'result: ' + (x + y)", 42 "result: 7" 43 ); 44 45 setInputValue(hud, "x + y"); 46 await waitForEagerEvaluationResult(hud, "7"); 47 48 setInputValue(hud, "x + y + undefined"); 49 await waitForEagerEvaluationResult(hud, "NaN"); 50 51 setInputValue(hud, "1 - 1"); 52 await waitForEagerEvaluationResult(hud, "0"); 53 54 setInputValue(hud, "!true"); 55 await waitForEagerEvaluationResult(hud, "false"); 56 57 setInputValue(hud, `"ab".slice(0, 0)`); 58 await waitForEagerEvaluationResult(hud, `""`); 59 60 setInputValue(hud, `JSON.parse("null")`); 61 await waitForEagerEvaluationResult(hud, "null"); 62 63 setInputValue(hud, "-x / 0"); 64 await waitForEagerEvaluationResult(hud, "-Infinity"); 65 66 setInputValue(hud, "x = 10"); 67 await waitForNoEagerEvaluationResult(hud); 68 69 setInputValue(hud, "x + 1"); 70 await waitForEagerEvaluationResult(hud, "4"); 71 72 setInputValue(hud, "zzyzx()"); 73 await waitForNoEagerEvaluationResult(hud); 74 75 setInputValue(hud, "x + 2"); 76 await waitForEagerEvaluationResult(hud, "5"); 77 78 setInputValue(hud, "x +"); 79 await waitForNoEagerEvaluationResult(hud); 80 81 setInputValue(hud, "x + z"); 82 await waitForEagerEvaluationResult(hud, /ReferenceError/); 83 84 setInputValue(hud, "var a = 5"); 85 await waitForNoEagerEvaluationResult(hud); 86 87 setInputValue(hud, "x + a"); 88 await waitForEagerEvaluationResult(hud, /ReferenceError/); 89 90 setInputValue(hud, '"foobar".slice(1, 5)'); 91 await waitForEagerEvaluationResult(hud, '"ooba"'); 92 93 setInputValue(hud, '"foobar".toString()'); 94 await waitForEagerEvaluationResult(hud, '"foobar"'); 95 96 setInputValue(hud, "(new Array()).push(3)"); 97 await waitForNoEagerEvaluationResult(hud); 98 99 setInputValue(hud, "(new Uint32Array([1,2,3])).includes(2)"); 100 await waitForEagerEvaluationResult(hud, "true"); 101 102 setInputValue(hud, "Math.round(3.2)"); 103 await waitForEagerEvaluationResult(hud, "3"); 104 105 info("Check web console commands"); 106 setInputValue(hud, "help()"); 107 await waitForNoEagerEvaluationResult(hud); 108 109 setInputValue(hud, "$0"); 110 await waitForEagerEvaluationResult(hud, `<h1>`); 111 112 setInputValue(hud, "$('html')"); 113 await waitForEagerEvaluationResult(hud, `<html>`); 114 115 setInputValue(hud, "$$"); 116 await waitForEagerEvaluationResult(hud, `42`); 117 118 info("Check that $_ wasn't polluted by eager evaluations"); 119 setInputValue(hud, "$_"); 120 await waitForEagerEvaluationResult(hud, `"result: 7"`); 121 122 setInputValue(hud, "'> ' + $_"); 123 await waitForEagerEvaluationResult(hud, `"> result: 7"`); 124 125 info("Switch to editor mode"); 126 await toggleLayout(hud); 127 await waitForEagerEvaluationResult(hud, `"> result: 7"`); 128 ok(true, "eager evaluation is still displayed in editor mode"); 129 130 setInputValue(hud, "4 + 7"); 131 await waitForEagerEvaluationResult(hud, "11"); 132 133 // go back to inline layout. 134 await toggleLayout(hud); 135 136 setInputValue(hud, "typeof new Proxy({}, {})"); 137 await waitForEagerEvaluationResult(hud, `"object"`); 138 139 setInputValue(hud, "typeof Proxy.revocable({}, {}).revoke"); 140 await waitForEagerEvaluationResult(hud, `"function"`); 141 142 setInputValue(hud, "Reflect.apply(() => 1, null, [])"); 143 await waitForEagerEvaluationResult(hud, "1"); 144 setInputValue( 145 hud, 146 `Reflect.apply(() => { 147 globalThis.sideEffect = true; 148 return 2; 149 }, null, [])` 150 ); 151 await waitForNoEagerEvaluationResult(hud); 152 153 setInputValue(hud, "Reflect.construct(Array, []).length"); 154 await waitForEagerEvaluationResult(hud, "0"); 155 setInputValue( 156 hud, 157 `Reflect.construct(function() { 158 globalThis.sideEffect = true; 159 }, [])` 160 ); 161 await waitForNoEagerEvaluationResult(hud); 162 163 setInputValue(hud, "Reflect.defineProperty({}, 'a', {value: 1})"); 164 await waitForNoEagerEvaluationResult(hud); 165 166 setInputValue(hud, "Reflect.deleteProperty({a: 1}, 'a')"); 167 await waitForNoEagerEvaluationResult(hud); 168 169 setInputValue(hud, "Reflect.get({a: 1}, 'a')"); 170 await waitForEagerEvaluationResult(hud, "1"); 171 setInputValue(hud, "Reflect.get({get a(){return 2}, 'a')"); 172 await waitForNoEagerEvaluationResult(hud); 173 174 setInputValue(hud, "Reflect.getOwnPropertyDescriptor({a: 1}, 'a').value"); 175 await waitForEagerEvaluationResult(hud, "1"); 176 setInputValue( 177 hud, 178 `Reflect.getOwnPropertyDescriptor( 179 new Proxy({ a: 2 }, { getOwnPropertyDescriptor() { 180 globalThis.sideEffect = true; 181 return { value: 2 }; 182 }}), 183 "a" 184 )` 185 ); 186 await waitForNoEagerEvaluationResult(hud); 187 188 setInputValue(hud, "Reflect.getPrototypeOf({}) === Object.prototype"); 189 await waitForEagerEvaluationResult(hud, "true"); 190 setInputValue( 191 hud, 192 `Reflect.getPrototypeOf( 193 new Proxy({}, { getPrototypeOf() { 194 globalThis.sideEffect = true; 195 return null; 196 }}) 197 )` 198 ); 199 await waitForNoEagerEvaluationResult(hud); 200 201 setInputValue(hud, "Reflect.has({a: 1}, 'a')"); 202 await waitForEagerEvaluationResult(hud, "true"); 203 setInputValue( 204 hud, 205 `Reflect.has( 206 new Proxy({ a: 2 }, { has() { 207 globalThis.sideEffect = true; 208 return true; 209 }}), "a" 210 )` 211 ); 212 await waitForNoEagerEvaluationResult(hud); 213 214 setInputValue(hud, "Reflect.isExtensible({})"); 215 await waitForEagerEvaluationResult(hud, "true"); 216 setInputValue( 217 hud, 218 `Reflect.isExtensible( 219 new Proxy({}, { isExtensible() { 220 globalThis.sideEffect = true; 221 return true; 222 }}) 223 )` 224 ); 225 await waitForNoEagerEvaluationResult(hud); 226 227 setInputValue(hud, "Reflect.ownKeys({a: 1})[0]"); 228 await waitForEagerEvaluationResult(hud, `"a"`); 229 setInputValue( 230 hud, 231 `Reflect.ownKeys( 232 new Proxy({}, { ownKeys() { 233 globalThis.sideEffect = true; 234 return ['a']; 235 }}) 236 )` 237 ); 238 await waitForNoEagerEvaluationResult(hud); 239 240 setInputValue(hud, "Reflect.preventExtensions({})"); 241 await waitForNoEagerEvaluationResult(hud); 242 243 setInputValue(hud, "Reflect.set({}, 'a', 1)"); 244 await waitForNoEagerEvaluationResult(hud); 245 246 setInputValue(hud, "Reflect.setPrototypeOf({}, null)"); 247 await waitForNoEagerEvaluationResult(hud); 248 249 setInputValue(hud, "[] instanceof Array"); 250 await waitForEagerEvaluationResult(hud, "true"); 251 252 setInputValue(hud, "Int8Array.from({length: 1})[0]"); 253 await waitForEagerEvaluationResult(hud, "0"); 254 255 setInputValue(hud, "Float64Array.of(1)[0]"); 256 await waitForEagerEvaluationResult(hud, "1"); 257 258 setInputValue(hud, "array.fill()"); 259 await waitForNoEagerEvaluationResult(hud); 260 261 setInputValue(hud, "array"); 262 await waitForEagerEvaluationResult(hud, "Array(3) [ 1, 2, 3 ]"); 263 264 info("Check that top-level await expression are not evaluated"); 265 setInputValue(hud, "await 1; 2 + 3;"); 266 await waitForNoEagerEvaluationResult(hud); 267 ok(true, "instant evaluation is disabled for top-level await expressions"); 268 269 info( 270 "Check that effect-less expression are eagerly evaluated even when going through contentWindow" 271 ); 272 setInputValue( 273 hud, 274 `document.querySelector("iframe").contentWindow.Function("return location.search")()` 275 ); 276 await waitForEagerEvaluationResult(hud, `"?html=in%20iframe"`); 277 ok(true, "contentWindow expression was eagerly evaluated"); 278 279 info( 280 "Check that effectful expression are not eagerly evaluated when going through contentWindow" 281 ); 282 setInputValue( 283 hud, 284 `document.querySelector("iframe").contentWindow.Function("globalThis.x = 10; return globalThis.x")()` 285 ); 286 await waitForNoEagerEvaluationResult(hud); 287 ok(true, "effectful contentWindow expression was not eagerly evaluated"); 288 // double check that the evaluation wasn't done 289 SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 290 is( 291 content.document.querySelector("iframe").contentWindow.x, 292 undefined, 293 "iframe global x property wasn't set" 294 ); 295 }); 296 297 info( 298 "Check that effect-less expression are eagerly evaluated even when going through window.parent" 299 ); 300 // First, select the iframe as the evaluation target 301 selectTargetInContextSelector( 302 hud, 303 "https://example.com/document-builder.sjs?html=in%20iframe" 304 ); 305 setInputValue( 306 hud, 307 ` 308 // sanity check to make sure we do evaluate this from the iframe document 309 if (globalThis.parent === globalThis) { 310 throw new Error("unexpected") 311 } 312 globalThis.parent.Function("return array")() 313 ` 314 ); 315 await waitForEagerEvaluationResult(hud, "Array(3) [ 1, 2, 3 ]"); 316 ok(true, "window.parent expression was eagerly evaluated"); 317 318 info( 319 "Check that effectful expression are not eagerly evaluated when going through window.parent" 320 ); 321 setInputValue( 322 hud, 323 ` 324 // sanity check to make sure we do evaluate this from the iframe document 325 if (globalThis.parent === globalThis) { 326 throw new Error("unexpected") 327 } 328 globalThis.parent.Function("return array.push(4)")() 329 ` 330 ); 331 await waitForNoEagerEvaluationResult(hud); 332 ok(true, "effectful window.parent expression was not eagerly evaluated"); 333 334 // double check that the evaluation wasn't done 335 SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 336 Assert.deepEqual( 337 content.wrappedJSObject.array, 338 [1, 2, 3], 339 "array property wasn't modified" 340 ); 341 }); 342 }); 343 344 // Test that the currently selected autocomplete result is eagerly evaluated. 345 add_task(async function () { 346 const hud = await openNewTabAndConsole(TEST_URI); 347 const { jsterm } = hud; 348 349 const { autocompletePopup: popup } = jsterm; 350 351 ok(!popup.isOpen, "popup is not open"); 352 let onPopupOpen = popup.once("popup-opened"); 353 EventUtils.sendString("zzy"); 354 await onPopupOpen; 355 356 await waitForEagerEvaluationResult(hud, "function zzyzx()"); 357 EventUtils.synthesizeKey("KEY_ArrowDown"); 358 await waitForEagerEvaluationResult(hud, "function zzyzx2()"); 359 360 // works when the input isn't properly cased but matches an autocomplete item 361 setInputValue(hud, "o"); 362 onPopupOpen = popup.once("popup-opened"); 363 EventUtils.sendString("B"); 364 await waitForEagerEvaluationResult(hud, `Object { propA: "A", propB: "B" }`); 365 366 // works when doing element access without quotes 367 setInputValue(hud, "obj[p"); 368 onPopupOpen = popup.once("popup-opened"); 369 EventUtils.sendString("RoP"); 370 await waitForEagerEvaluationResult(hud, `"A"`); 371 372 EventUtils.synthesizeKey("KEY_ArrowDown"); 373 await waitForEagerEvaluationResult(hud, `"B"`); 374 375 // closing the autocomplete popup updates the eager evaluation result 376 let onPopupClose = popup.once("popup-closed"); 377 EventUtils.synthesizeKey("KEY_Escape"); 378 await onPopupClose; 379 await waitForNoEagerEvaluationResult(hud); 380 381 info( 382 "Check that closing the popup by adding a space will update the instant eval result" 383 ); 384 await setInputValueForAutocompletion(hud, "x"); 385 await waitForEagerEvaluationResult(hud, "3"); 386 387 EventUtils.synthesizeKey("KEY_ArrowDown"); 388 // Navigates to the XMLDocument item in the popup 389 await waitForEagerEvaluationResult(hud, `function XMLDocument()`); 390 391 onPopupClose = popup.once("popup-closed"); 392 EventUtils.sendString(" "); 393 await waitForEagerEvaluationResult(hud, `3`); 394 }); 395 396 // Test that the setting works as expected. 397 add_task(async function () { 398 // start with the pref off. 399 await pushPref(EAGER_EVALUATION_PREF, false); 400 const hud = await openNewTabAndConsole(TEST_URI); 401 402 info("Check that the setting is disabled"); 403 checkConsoleSettingState( 404 hud, 405 ".webconsole-console-settings-menu-item-eager-evaluation", 406 false 407 ); 408 409 // Wait for the autocomplete popup to be displayed so we know the eager evaluation could 410 // have occured. 411 const onPopupOpen = hud.jsterm.autocompletePopup.once("popup-opened"); 412 await setInputValueForAutocompletion(hud, "x + y"); 413 await onPopupOpen; 414 415 is( 416 getEagerEvaluationElement(hud), 417 null, 418 "There's no eager evaluation element" 419 ); 420 hud.jsterm.autocompletePopup.hidePopup(); 421 422 info("Turn on the eager evaluation"); 423 toggleConsoleSetting( 424 hud, 425 ".webconsole-console-settings-menu-item-eager-evaluation" 426 ); 427 await waitFor(() => getEagerEvaluationElement(hud)); 428 ok(true, "The eager evaluation element is now displayed"); 429 is( 430 Services.prefs.getBoolPref(EAGER_EVALUATION_PREF), 431 true, 432 "Pref was changed" 433 ); 434 435 setInputValue(hud, "1 + 2"); 436 await waitForEagerEvaluationResult(hud, "3"); 437 ok(true, "Eager evaluation result is displayed"); 438 439 info("Turn off the eager evaluation"); 440 toggleConsoleSetting( 441 hud, 442 ".webconsole-console-settings-menu-item-eager-evaluation" 443 ); 444 await waitFor(() => !getEagerEvaluationElement(hud)); 445 is( 446 Services.prefs.getBoolPref(EAGER_EVALUATION_PREF), 447 false, 448 "Pref was changed" 449 ); 450 ok(true, "Eager evaluation element is no longer displayed"); 451 452 // reset the preference 453 await pushPref(EAGER_EVALUATION_PREF, true); 454 }); 455 456 // Test that the console instant evaluation is updated on page navigation 457 add_task(async function () { 458 const start_uri = "data:text/html, Start uri"; 459 const new_uri = "data:text/html, Test console refresh instant value"; 460 const hud = await openNewTabAndConsole(start_uri); 461 462 setInputValue(hud, "globalThis.location.href"); 463 await waitForEagerEvaluationResult(hud, `"${start_uri}"`); 464 465 await navigateTo(new_uri); 466 await waitForEagerEvaluationResult(hud, `"${new_uri}"`); 467 });