tor-browser

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

head.js (15549B)


      1 "use strict";
      2 
      3 // This file expects these globals to be defined by the test case.
      4 /* global gTestTab:true, gContentAPI:true, tests:false */
      5 
      6 ChromeUtils.defineESModuleGetters(this, {
      7  UITour: "moz-src:///browser/components/uitour/UITour.sys.mjs",
      8 });
      9 
     10 const { PermissionTestUtils } = ChromeUtils.importESModule(
     11  "resource://testing-common/PermissionTestUtils.sys.mjs"
     12 );
     13 
     14 const SINGLE_TRY_TIMEOUT = 100;
     15 const NUMBER_OF_TRIES = 30;
     16 
     17 let gProxyCallbackMap = new Map();
     18 
     19 function waitForConditionPromise(
     20  condition,
     21  timeoutMsg,
     22  tryCount = NUMBER_OF_TRIES
     23 ) {
     24  return new Promise((resolve, reject) => {
     25    let tries = 0;
     26    function checkCondition() {
     27      if (tries >= tryCount) {
     28        reject(timeoutMsg);
     29      }
     30      var conditionPassed;
     31      try {
     32        conditionPassed = condition();
     33      } catch (e) {
     34        return reject(e);
     35      }
     36      if (conditionPassed) {
     37        return resolve();
     38      }
     39      tries++;
     40      setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
     41      return undefined;
     42    }
     43    setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
     44  });
     45 }
     46 
     47 function waitForCondition(condition, nextTestFn, errorMsg) {
     48  waitForConditionPromise(condition, errorMsg).then(nextTestFn, reason => {
     49    ok(false, reason + (reason.stack ? "\n" + reason.stack : ""));
     50  });
     51 }
     52 
     53 /**
     54 * Wrapper to partially transition tests to Task. Use `add_UITour_task` instead for new tests.
     55 */
     56 function taskify(fun) {
     57  return doneFn => {
     58    // Output the inner function name otherwise no name will be output.
     59    info("\t" + fun.name);
     60    return fun().then(doneFn, reason => {
     61      console.error(reason);
     62      ok(false, reason);
     63      doneFn();
     64    });
     65  };
     66 }
     67 
     68 function is_hidden(element) {
     69  let win = element.ownerGlobal;
     70  let style = win.getComputedStyle(element);
     71  if (style.display == "none") {
     72    return true;
     73  }
     74  if (style.visibility != "visible") {
     75    return true;
     76  }
     77  if (win.XULPopupElement.isInstance(element)) {
     78    return ["hiding", "closed"].includes(element.state);
     79  }
     80 
     81  // Hiding a parent element will hide all its children
     82  if (element.parentNode != element.ownerDocument) {
     83    return is_hidden(element.parentNode);
     84  }
     85 
     86  return false;
     87 }
     88 
     89 function is_visible(element) {
     90  let win = element.ownerGlobal;
     91  let style = win.getComputedStyle(element);
     92  if (style.display == "none") {
     93    return false;
     94  }
     95  if (style.visibility != "visible") {
     96    return false;
     97  }
     98  if (win.XULPopupElement.isInstance(element) && element.state != "open") {
     99    return false;
    100  }
    101 
    102  // Hiding a parent element will hide all its children
    103  if (element.parentNode != element.ownerDocument) {
    104    return is_visible(element.parentNode);
    105  }
    106 
    107  return true;
    108 }
    109 
    110 function is_element_visible(element, msg) {
    111  isnot(element, null, "Element should not be null, when checking visibility");
    112  ok(is_visible(element), msg);
    113 }
    114 
    115 function waitForElementToBeVisible(element, nextTestFn, msg) {
    116  waitForCondition(
    117    () => is_visible(element),
    118    () => {
    119      ok(true, msg);
    120      nextTestFn();
    121    },
    122    "Timeout waiting for visibility: " + msg
    123  );
    124 }
    125 
    126 function waitForElementToBeHidden(element, nextTestFn, msg) {
    127  waitForCondition(
    128    () => is_hidden(element),
    129    () => {
    130      ok(true, msg);
    131      nextTestFn();
    132    },
    133    "Timeout waiting for invisibility: " + msg
    134  );
    135 }
    136 
    137 function elementVisiblePromise(element, msg) {
    138  return waitForConditionPromise(
    139    () => is_visible(element),
    140    "Timeout waiting for visibility: " + msg
    141  );
    142 }
    143 
    144 function elementHiddenPromise(element, msg) {
    145  return waitForConditionPromise(
    146    () => is_hidden(element),
    147    "Timeout waiting for invisibility: " + msg
    148  );
    149 }
    150 
    151 function waitForPopupAtAnchor(popup, anchorNode, nextTestFn, msg) {
    152  waitForCondition(
    153    () => is_visible(popup) && popup.anchorNode == anchorNode,
    154    () => {
    155      ok(true, msg);
    156      is_element_visible(popup, "Popup should be visible");
    157      nextTestFn();
    158    },
    159    "Timeout waiting for popup at anchor: " + msg
    160  );
    161 }
    162 
    163 function getConfigurationPromise(configName) {
    164  return SpecialPowers.spawn(
    165    gTestTab.linkedBrowser,
    166    [configName],
    167    contentConfigName => {
    168      return new Promise(resolve => {
    169        let contentWin = Cu.waiveXrays(content);
    170        contentWin.Mozilla.UITour.getConfiguration(contentConfigName, resolve);
    171      });
    172    }
    173  );
    174 }
    175 
    176 function getShowHighlightTargetName() {
    177  let highlight = document.getElementById("UITourHighlight");
    178  return highlight.parentElement.getAttribute("targetName");
    179 }
    180 
    181 function getShowInfoTargetName() {
    182  let tooltip = document.getElementById("UITourTooltip");
    183  return tooltip.getAttribute("targetName");
    184 }
    185 
    186 function hideInfoPromise(...args) {
    187  let popup = document.getElementById("UITourTooltip");
    188  gContentAPI.hideInfo.apply(gContentAPI, args);
    189  return promisePanelElementHidden(window, popup);
    190 }
    191 
    192 /**
    193 * `buttons` and `options` require functions from the content scope so we take a
    194 * function name to call to generate the buttons/options instead of the
    195 * buttons/options themselves. This makes the signature differ from the content one.
    196 */
    197 function showInfoPromise() {
    198  let popup = document.getElementById("UITourTooltip");
    199  let shownPromise = promisePanelElementShown(window, popup);
    200  return SpecialPowers.spawn(gTestTab.linkedBrowser, [[...arguments]], args => {
    201    let contentWin = Cu.waiveXrays(content);
    202    let [
    203      contentTarget,
    204      contentTitle,
    205      contentText,
    206      contentIcon,
    207      contentButtonsFunctionName,
    208      contentOptionsFunctionName,
    209    ] = args;
    210    let buttons = contentButtonsFunctionName
    211      ? contentWin[contentButtonsFunctionName]()
    212      : null;
    213    let options = contentOptionsFunctionName
    214      ? contentWin[contentOptionsFunctionName]()
    215      : null;
    216    contentWin.Mozilla.UITour.showInfo(
    217      contentTarget,
    218      contentTitle,
    219      contentText,
    220      contentIcon,
    221      buttons,
    222      options
    223    );
    224  }).then(() => shownPromise);
    225 }
    226 
    227 function showHighlightPromise(...args) {
    228  let popup = document.getElementById("UITourHighlightContainer");
    229  gContentAPI.showHighlight.apply(gContentAPI, args);
    230  return promisePanelElementShown(window, popup);
    231 }
    232 
    233 function showMenuPromise(name) {
    234  return SpecialPowers.spawn(gTestTab.linkedBrowser, [name], contentName => {
    235    return new Promise(resolve => {
    236      let contentWin = Cu.waiveXrays(content);
    237      contentWin.Mozilla.UITour.showMenu(contentName, resolve);
    238    });
    239  });
    240 }
    241 
    242 function waitForCallbackResultPromise() {
    243  return SpecialPowers.spawn(gTestTab.linkedBrowser, [], async function () {
    244    let contentWin = Cu.waiveXrays(content);
    245    await ContentTaskUtils.waitForCondition(() => {
    246      return contentWin.callbackResult;
    247    }, "callback should be called");
    248    return {
    249      data: contentWin.callbackData,
    250      result: contentWin.callbackResult,
    251    };
    252  });
    253 }
    254 
    255 function promisePanelShown(win) {
    256  let panelEl = win.PanelUI.panel;
    257  return promisePanelElementShown(win, panelEl);
    258 }
    259 
    260 function promisePanelElementEvent(win, aPanel, aEvent) {
    261  return new Promise((resolve, reject) => {
    262    let timeoutId = win.setTimeout(() => {
    263      aPanel.removeEventListener(aEvent, onPanelEvent);
    264      reject(aEvent + " event did not happen within 5 seconds.");
    265    }, 5000);
    266 
    267    function onPanelEvent() {
    268      aPanel.removeEventListener(aEvent, onPanelEvent);
    269      win.clearTimeout(timeoutId);
    270      // Wait one tick to let UITour.sys.mjs process the event as well.
    271      executeSoon(resolve);
    272    }
    273 
    274    aPanel.addEventListener(aEvent, onPanelEvent);
    275  });
    276 }
    277 
    278 function promisePanelElementShown(win, aPanel) {
    279  return promisePanelElementEvent(win, aPanel, "popupshown");
    280 }
    281 
    282 function promisePanelElementHidden(win, aPanel) {
    283  return promisePanelElementEvent(win, aPanel, "popuphidden");
    284 }
    285 
    286 function is_element_hidden(element, msg) {
    287  isnot(element, null, "Element should not be null, when checking visibility");
    288  ok(is_hidden(element), msg);
    289 }
    290 
    291 function isTourBrowser(aBrowser) {
    292  let chromeWindow = aBrowser.ownerGlobal;
    293  return (
    294    UITour.tourBrowsersByWindow.has(chromeWindow) &&
    295    UITour.tourBrowsersByWindow.get(chromeWindow).has(aBrowser)
    296  );
    297 }
    298 
    299 async function loadUITourTestPage(callback, host = "https://example.org/") {
    300  if (gTestTab) {
    301    gProxyCallbackMap.clear();
    302    gBrowser.removeTab(gTestTab);
    303  }
    304 
    305  if (!window.gProxyCallbackMap) {
    306    window.gProxyCallbackMap = gProxyCallbackMap;
    307  }
    308 
    309  let url = getRootDirectory(gTestPath) + "uitour.html";
    310  url = url.replace("chrome://mochitests/content/", host);
    311 
    312  gTestTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
    313  // When e10s is enabled, make gContentAPI a proxy which has every property
    314  // return a function which calls the method of the same name on
    315  // contentWin.Mozilla.UITour in a ContentTask.
    316  let UITourHandler = {
    317    get(target, prop) {
    318      return (...args) => {
    319        let browser = gTestTab.linkedBrowser;
    320        // We need to proxy any callback functions using messages:
    321        let fnIndices = [];
    322        args = args.map((arg, index) => {
    323          // Replace function arguments with "", and add them to the list of
    324          // forwarded functions. We'll construct a function on the content-side
    325          // that forwards all its arguments to a message, and we'll listen for
    326          // those messages on our side and call the corresponding function with
    327          // the arguments we got from the content side.
    328          if (typeof arg == "function") {
    329            gProxyCallbackMap.set(index, arg);
    330            fnIndices.push(index);
    331            return "";
    332          }
    333          return arg;
    334        });
    335        let taskArgs = {
    336          methodName: prop,
    337          args,
    338          fnIndices,
    339        };
    340        return SpecialPowers.spawn(
    341          browser,
    342          [taskArgs],
    343          async function (contentArgs) {
    344            let contentWin = Cu.waiveXrays(content);
    345            let callbacksCalled = 0;
    346            let resolveCallbackPromise;
    347            let allCallbacksCalledPromise = new Promise(
    348              resolve => (resolveCallbackPromise = resolve)
    349            );
    350            let argumentsWithFunctions = Cu.cloneInto(
    351              contentArgs.args.map((arg, index) => {
    352                if (arg === "" && contentArgs.fnIndices.includes(index)) {
    353                  return function () {
    354                    callbacksCalled++;
    355                    SpecialPowers.spawnChrome(
    356                      [index, Array.from(arguments)],
    357                      (indexParent, argumentsParent) => {
    358                        // Please note that this handler only allows the callback to be used once.
    359                        // That means that a single gContentAPI.observer() call can't be used
    360                        // to observe multiple events.
    361                        let window = this.browsingContext.topChromeWindow;
    362                        let cb = window.gProxyCallbackMap.get(indexParent);
    363                        window.gProxyCallbackMap.delete(indexParent);
    364                        cb.apply(null, argumentsParent);
    365                      }
    366                    );
    367                    if (callbacksCalled >= contentArgs.fnIndices.length) {
    368                      resolveCallbackPromise();
    369                    }
    370                  };
    371                }
    372                return arg;
    373              }),
    374              content,
    375              { cloneFunctions: true }
    376            );
    377            let rv = contentWin.Mozilla.UITour[contentArgs.methodName].apply(
    378              contentWin.Mozilla.UITour,
    379              argumentsWithFunctions
    380            );
    381            if (contentArgs.fnIndices.length) {
    382              await allCallbacksCalledPromise;
    383            }
    384            return rv;
    385          }
    386        );
    387      };
    388    },
    389  };
    390  gContentAPI = new Proxy({}, UITourHandler);
    391 
    392  await SimpleTest.promiseFocus(gTestTab.linkedBrowser);
    393  callback();
    394 }
    395 
    396 // Wrapper for UITourTest to be used by add_task tests.
    397 function setup_UITourTest() {
    398  return UITourTest(true);
    399 }
    400 
    401 // Use `add_task(setup_UITourTest);` instead as we will fold this into `setup_UITourTest` once all tests are using `add_UITour_task`.
    402 function UITourTest(usingAddTask = false) {
    403  Services.prefs.setBoolPref("browser.uitour.enabled", true);
    404  let testHttpsOrigin = "https://example.org";
    405  let testHttpOrigin = "http://example.org";
    406  PermissionTestUtils.add(
    407    testHttpsOrigin,
    408    "uitour",
    409    Services.perms.ALLOW_ACTION
    410  );
    411  PermissionTestUtils.add(
    412    testHttpOrigin,
    413    "uitour",
    414    Services.perms.ALLOW_ACTION
    415  );
    416 
    417  UITour.getHighlightContainerAndMaybeCreate(window.document);
    418  UITour.getTooltipAndMaybeCreate(window.document);
    419 
    420  // If a test file is using add_task, we don't need to have a test function or
    421  // call `waitForExplicitFinish`.
    422  if (!usingAddTask) {
    423    waitForExplicitFinish();
    424  }
    425 
    426  registerCleanupFunction(function () {
    427    delete window.gContentAPI;
    428    if (gTestTab) {
    429      gBrowser.removeTab(gTestTab);
    430    }
    431    delete window.gTestTab;
    432    delete window.gProxyCallbackMap;
    433    Services.prefs.clearUserPref("browser.uitour.enabled");
    434    PermissionTestUtils.remove(testHttpsOrigin, "uitour");
    435    PermissionTestUtils.remove(testHttpOrigin, "uitour");
    436  });
    437 
    438  // When using tasks, the harness will call the next added task for us.
    439  if (!usingAddTask) {
    440    nextTest();
    441  }
    442 }
    443 
    444 function done(usingAddTask = false) {
    445  info("== Done test, doing shared checks before teardown ==");
    446  return new Promise(resolve => {
    447    executeSoon(() => {
    448      if (gTestTab) {
    449        gBrowser.removeTab(gTestTab);
    450      }
    451      gTestTab = null;
    452      gProxyCallbackMap.clear();
    453 
    454      let highlight = document.getElementById("UITourHighlightContainer");
    455      is_element_hidden(
    456        highlight,
    457        "Highlight should be closed/hidden after UITour tab is closed"
    458      );
    459 
    460      let tooltip = document.getElementById("UITourTooltip");
    461      is_element_hidden(
    462        tooltip,
    463        "Tooltip should be closed/hidden after UITour tab is closed"
    464      );
    465 
    466      ok(
    467        !PanelUI.panel.hasAttribute("noautohide"),
    468        "@noautohide on the menu panel should have been cleaned up"
    469      );
    470      ok(
    471        !PanelUI.panel.hasAttribute("panelopen"),
    472        "The panel shouldn't have @panelopen"
    473      );
    474      isnot(PanelUI.panel.state, "open", "The panel shouldn't be open");
    475      is(
    476        document.getElementById("PanelUI-menu-button").hasAttribute("open"),
    477        false,
    478        "Menu button should know that the menu is closed"
    479      );
    480 
    481      info("Done shared checks");
    482      if (usingAddTask) {
    483        executeSoon(resolve);
    484      } else {
    485        executeSoon(nextTest);
    486      }
    487    });
    488  });
    489 }
    490 
    491 function nextTest() {
    492  if (!tests.length) {
    493    info("finished tests in this file");
    494    finish();
    495    return;
    496  }
    497  let test = tests.shift();
    498  info("Starting " + test.name);
    499  waitForFocus(function () {
    500    loadUITourTestPage(function () {
    501      test(done);
    502    });
    503  });
    504 }
    505 
    506 /**
    507 * All new tests that need the help of `loadUITourTestPage` should use this
    508 * wrapper around their test's generator function to reduce boilerplate.
    509 */
    510 function add_UITour_task(func) {
    511  let genFun = async function () {
    512    await new Promise(resolve => {
    513      waitForFocus(function () {
    514        loadUITourTestPage(function () {
    515          let funcPromise = (func() || Promise.resolve()).then(
    516            () => done(true),
    517            reason => {
    518              ok(false, reason);
    519              return done(true);
    520            }
    521          );
    522          resolve(funcPromise);
    523        });
    524      });
    525    });
    526  };
    527  Object.defineProperty(genFun, "name", {
    528    configurable: true,
    529    value: func.name,
    530  });
    531  add_task(genFun);
    532 }