tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });