tor-browser

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

docshell_helpers.js (23852B)


      1 if (!window.opener && window.arguments) {
      2  window.opener = window.arguments[0];
      3 }
      4 /**
      5 * Import common SimpleTest methods so that they're usable in this window.
      6 */
      7 /* globals SimpleTest, is, isnot, ok, onerror, todo, todo_is, todo_isnot */
      8 var imports = [
      9  "SimpleTest",
     10  "is",
     11  "isnot",
     12  "ok",
     13  "onerror",
     14  "todo",
     15  "todo_is",
     16  "todo_isnot",
     17 ];
     18 for (var name of imports) {
     19  window[name] = window.opener.wrappedJSObject[name];
     20 }
     21 const { BrowserTestUtils } = ChromeUtils.importESModule(
     22  "resource://testing-common/BrowserTestUtils.sys.mjs"
     23 );
     24 
     25 const ACTOR_MODULE_URI =
     26  "chrome://mochitests/content/chrome/docshell/test/chrome/DocShellHelpers.sys.mjs";
     27 const { DocShellHelpersParent } = ChromeUtils.importESModule(ACTOR_MODULE_URI);
     28 // Some functions assume chrome-harness.js has been loaded.
     29 /* import-globals-from ../../../testing/mochitest/chrome-harness.js */
     30 
     31 /**
     32 * Define global constants and variables.
     33 */
     34 const NAV_NONE = 0;
     35 const NAV_BACK = 1;
     36 const NAV_FORWARD = 2;
     37 const NAV_GOTOINDEX = 3;
     38 const NAV_URI = 4;
     39 const NAV_RELOAD = 5;
     40 
     41 var gExpectedEvents; // an array of events which are expected to
     42 // be triggered by this navigation
     43 var gUnexpectedEvents; // an array of event names which are NOT expected
     44 // to be triggered by this navigation
     45 var gFinalEvent; // true if the last expected event has fired
     46 var gUrisNotInBFCache = []; // an array of uri's which shouldn't be stored
     47 // in the bfcache
     48 var gNavType = NAV_NONE; // defines the most recent navigation type
     49 // executed by doPageNavigation
     50 var gOrigMaxTotalViewers = undefined; // original value of max_total_viewers, // to be restored at end of test
     51 
     52 var gExtractedPath = null; // used to cache file path for extracting files from a .jar file
     53 
     54 /**
     55 * The doPageNavigation() function performs page navigations asynchronously,
     56 * listens for specified events, and compares actual events with a list of
     57 * expected events.  When all expected events have occurred, an optional
     58 * callback can be notified. The parameter passed to this function is an
     59 * object with the following properties:
     60 *
     61 *                uri: if !undefined, the browser will navigate to this uri
     62 *
     63 *               back: if true, the browser will execute goBack()
     64 *
     65 *            forward: if true, the browser will execute goForward()
     66 *
     67 *          gotoIndex: if a number, the browser will execute gotoIndex() with
     68 *                     the number as index
     69 *
     70 *             reload: if true, the browser will execute reload()
     71 *
     72 *  eventsToListenFor: an array containing one or more of the following event
     73 *                     types to listen for:  "pageshow", "pagehide", "onload",
     74 *                     "onunload".  If this property is undefined, only a
     75 *                     single "pageshow" events will be listened for.  If this
     76 *                     property is explicitly empty, [], then no events will
     77 *                     be listened for.
     78 *
     79 *     expectedEvents: an array of one or more expectedEvent objects,
     80 *                     corresponding to the events which are expected to be
     81 *                     fired for this navigation.  Each object has the
     82 *                     following properties:
     83 *
     84 *                          type: one of the event type strings
     85 *                          title (optional): the title of the window the
     86 *                              event belongs to
     87 *                          persisted (optional): the event's expected
     88 *                              .persisted attribute
     89 *
     90 *                     This function will verify that events with the
     91 *                     specified properties are fired in the same order as
     92 *                     specified in the array.  If .title or .persisted
     93 *                     properties for an expectedEvent are undefined, those
     94 *                     properties will not be verified for that particular
     95 *                     event.
     96 *
     97 *                     This property is ignored if eventsToListenFor is
     98 *                     undefined or [].
     99 *
    100 *     preventBFCache: if true, an RTCPeerConnection will be added to the loaded
    101 *                     page to prevent it from being bfcached.  This property
    102 *                     has no effect when eventsToListenFor is [].
    103 *
    104 *      onNavComplete: a callback which is notified after all expected events
    105 *                     have occurred, or after a timeout has elapsed.  This
    106 *                     callback is not notified if eventsToListenFor is [].
    107 *   onGlobalCreation: a callback which is notified when a DOMWindow is created
    108 *                     (implemented by observing
    109 *                     "content-document-global-created")
    110 *
    111 * There must be an expectedEvent object for each event of the types in
    112 * eventsToListenFor which is triggered by this navigation.  For example, if
    113 * eventsToListenFor = [ "pagehide", "pageshow" ], then expectedEvents
    114 * must contain an object for each pagehide and pageshow event which occurs as
    115 * a result of this navigation.
    116 */
    117 // eslint-disable-next-line complexity
    118 function doPageNavigation(params) {
    119  // Parse the parameters.
    120  let back = params.back ? params.back : false;
    121  let forward = params.forward ? params.forward : false;
    122  let gotoIndex = params.gotoIndex ? params.gotoIndex : false;
    123  let reload = params.reload ? params.reload : false;
    124  let uri = params.uri ? params.uri : false;
    125  let eventsToListenFor =
    126    typeof params.eventsToListenFor != "undefined"
    127      ? params.eventsToListenFor
    128      : ["pageshow"];
    129  gExpectedEvents =
    130    typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length
    131      ? undefined
    132      : params.expectedEvents;
    133  gUnexpectedEvents =
    134    typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length
    135      ? undefined
    136      : params.unexpectedEvents;
    137  let preventBFCache =
    138    typeof [params.preventBFCache] == "undefined"
    139      ? false
    140      : params.preventBFCache;
    141  let waitOnly =
    142    typeof params.waitForEventsOnly == "boolean" && params.waitForEventsOnly;
    143 
    144  // Do some sanity checking on arguments.
    145  let navigation = ["back", "forward", "gotoIndex", "reload", "uri"].filter(k =>
    146    params.hasOwnProperty(k)
    147  );
    148  if (navigation.length > 1) {
    149    throw new Error(`Can't specify both ${navigation[0]} and ${navigation[1]}`);
    150  } else if (!navigation.length && !waitOnly) {
    151    throw new Error(
    152      "Must specify back or forward or gotoIndex or reload or uri"
    153    );
    154  }
    155  if (params.onNavComplete && !eventsToListenFor.length) {
    156    throw new Error("Can't use onNavComplete when eventsToListenFor == []");
    157  }
    158  if (params.preventBFCache && !eventsToListenFor.length) {
    159    throw new Error("Can't use preventBFCache when eventsToListenFor == []");
    160  }
    161  if (params.preventBFCache && waitOnly) {
    162    throw new Error("Can't prevent bfcaching when only waiting for events");
    163  }
    164  if (waitOnly && typeof params.onNavComplete == "undefined") {
    165    throw new Error(
    166      "Must specify onNavComplete when specifying waitForEventsOnly"
    167    );
    168  }
    169  if (waitOnly && navigation.length) {
    170    throw new Error(
    171      "Can't specify a navigation type when using waitForEventsOnly"
    172    );
    173  }
    174  for (let anEventType of eventsToListenFor) {
    175    let eventFound = false;
    176    if (anEventType == "pageshow" && !gExpectedEvents) {
    177      eventFound = true;
    178    }
    179    if (gExpectedEvents) {
    180      for (let anExpectedEvent of gExpectedEvents) {
    181        if (anExpectedEvent.type == anEventType) {
    182          eventFound = true;
    183        }
    184      }
    185    }
    186    if (gUnexpectedEvents) {
    187      for (let anExpectedEventType of gUnexpectedEvents) {
    188        if (anExpectedEventType == anEventType) {
    189          eventFound = true;
    190        }
    191      }
    192    }
    193    if (!eventFound) {
    194      throw new Error(
    195        `Event type ${anEventType} is specified in ` +
    196          "eventsToListenFor, but not in expectedEvents"
    197      );
    198    }
    199  }
    200 
    201  // If the test explicitly sets .eventsToListenFor to [], don't wait for any
    202  // events.
    203  gFinalEvent = !eventsToListenFor.length;
    204 
    205  // Add observers as needed.
    206  let observers = new Map();
    207  if (params.hasOwnProperty("onGlobalCreation")) {
    208    observers.set("content-document-global-created", params.onGlobalCreation);
    209  }
    210 
    211  // Add an event listener for each type of event in the .eventsToListenFor
    212  // property of the input parameters, and add an observer for all the topics
    213  // in the observers map.
    214  let cleanup;
    215  let useActor = TestWindow.getBrowser().isRemoteBrowser;
    216  if (useActor) {
    217    ChromeUtils.registerWindowActor("DocShellHelpers", {
    218      parent: {
    219        esModuleURI: ACTOR_MODULE_URI,
    220      },
    221      child: {
    222        esModuleURI: ACTOR_MODULE_URI,
    223        events: {
    224          pageshow: { createActor: true, capture: true },
    225          pagehide: { createActor: true, capture: true },
    226          load: { createActor: true, capture: true },
    227          unload: { createActor: true, capture: true },
    228          visibilitychange: { createActor: true, capture: true },
    229        },
    230        observers: observers.keys(),
    231      },
    232      allFrames: true,
    233      // We avoid messages from system addons event pages here, as
    234      // the tests test_bug321671.xhtml and test_bug690056.xhtml do
    235      // not expect those events, and so will intermittently fail.
    236      // They require messages from "browsers", "test", and "" to pass.
    237      // See bug 1784831 and bug 1883434 for more context.
    238      messageManagerGroups: ["browsers", "test", ""],
    239    });
    240    DocShellHelpersParent.eventsToListenFor = eventsToListenFor;
    241    DocShellHelpersParent.observers = observers;
    242 
    243    cleanup = () => {
    244      DocShellHelpersParent.eventsToListenFor = null;
    245      DocShellHelpersParent.observers = null;
    246      ChromeUtils.unregisterWindowActor("DocShellHelpers");
    247    };
    248  } else {
    249    for (let eventType of eventsToListenFor) {
    250      dump("TEST: registering a listener for " + eventType + " events\n");
    251      TestWindow.getBrowser().addEventListener(
    252        eventType,
    253        pageEventListener,
    254        true
    255      );
    256    }
    257    if (observers.size > 0) {
    258      let observer = (_, topic) => {
    259        observers.get(topic).call();
    260      };
    261      for (let topic of observers.keys()) {
    262        Services.obs.addObserver(observer, topic);
    263      }
    264 
    265      // We only need to do cleanup for the observer, the event listeners will
    266      // go away with the window.
    267      cleanup = () => {
    268        for (let topic of observers.keys()) {
    269          Services.obs.removeObserver(observer, topic);
    270        }
    271      };
    272    }
    273  }
    274 
    275  if (cleanup) {
    276    // Register a cleanup function on domwindowclosed, to avoid contaminating
    277    // other tests if we bail out early because of an error.
    278    Services.ww.registerNotification(function windowClosed(subject, topic) {
    279      if (topic == "domwindowclosed" && subject == window) {
    280        Services.ww.unregisterNotification(windowClosed);
    281        cleanup();
    282      }
    283    });
    284  }
    285 
    286  // Perform the specified navigation.
    287  if (back) {
    288    gNavType = NAV_BACK;
    289    TestWindow.getBrowser().goBack();
    290  } else if (forward) {
    291    gNavType = NAV_FORWARD;
    292    TestWindow.getBrowser().goForward();
    293  } else if (typeof gotoIndex == "number") {
    294    gNavType = NAV_GOTOINDEX;
    295    TestWindow.getBrowser().gotoIndex(gotoIndex);
    296  } else if (uri) {
    297    gNavType = NAV_URI;
    298    BrowserTestUtils.startLoadingURIString(TestWindow.getBrowser(), uri);
    299  } else if (reload) {
    300    gNavType = NAV_RELOAD;
    301    TestWindow.getBrowser().reload();
    302  } else if (waitOnly) {
    303    gNavType = NAV_NONE;
    304  } else {
    305    throw new Error("No valid navigation type passed to doPageNavigation!");
    306  }
    307 
    308  // If we're listening for events and there is an .onNavComplete callback,
    309  // wait for all events to occur, and then call doPageNavigation_complete().
    310  if (eventsToListenFor.length && params.onNavComplete) {
    311    waitForTrue(
    312      function () {
    313        return gFinalEvent;
    314      },
    315      function () {
    316        doPageNavigation_complete(
    317          eventsToListenFor,
    318          params.onNavComplete,
    319          preventBFCache,
    320          useActor,
    321          cleanup
    322        );
    323      }
    324    );
    325  } else if (cleanup) {
    326    cleanup();
    327  }
    328 }
    329 
    330 /**
    331 * Finish doPageNavigation(), by removing event listeners, adding an unload
    332 * handler if appropriate, and calling the onNavComplete callback.  This
    333 * function is called after all the expected events for this navigation have
    334 * occurred.
    335 */
    336 function doPageNavigation_complete(
    337  eventsToListenFor,
    338  onNavComplete,
    339  preventBFCache,
    340  useActor,
    341  cleanup
    342 ) {
    343  if (useActor) {
    344    if (preventBFCache) {
    345      let actor =
    346        TestWindow.getBrowser().browsingContext.currentWindowGlobal.getActor(
    347          "DocShellHelpers"
    348        );
    349      actor.sendAsyncMessage("docshell_helpers:preventBFCache");
    350    }
    351  } else {
    352    // Unregister our event listeners.
    353    dump("TEST: removing event listeners\n");
    354    for (let eventType of eventsToListenFor) {
    355      TestWindow.getBrowser().removeEventListener(
    356        eventType,
    357        pageEventListener,
    358        true
    359      );
    360    }
    361 
    362    // If the .preventBFCache property was set, add an RTCPeerConnection to
    363    // prevent the page from being bfcached.
    364    if (preventBFCache) {
    365      let win = TestWindow.getWindow();
    366      win.blockBFCache = new win.RTCPeerConnection();
    367    }
    368  }
    369 
    370  if (cleanup) {
    371    cleanup();
    372  }
    373 
    374  let uri = TestWindow.getBrowser().currentURI.spec;
    375  if (preventBFCache) {
    376    // Save the current uri in an array of uri's which shouldn't be
    377    // stored in the bfcache, for later verification.
    378    if (!(uri in gUrisNotInBFCache)) {
    379      gUrisNotInBFCache.push(uri);
    380    }
    381  } else if (gNavType == NAV_URI) {
    382    // If we're navigating to a uri and .preventBFCache was not
    383    // specified, splice it out of gUrisNotInBFCache if it's there.
    384    gUrisNotInBFCache.forEach(function (element, index, array) {
    385      if (element == uri) {
    386        array.splice(index, 1);
    387      }
    388    }, this);
    389  }
    390 
    391  // Notify the callback now that we're done.
    392  onNavComplete.call();
    393 }
    394 
    395 function promisePageNavigation(params) {
    396  if (params.hasOwnProperty("onNavComplete")) {
    397    throw new Error(
    398      "Can't use a onNavComplete completion callback with promisePageNavigation."
    399    );
    400  }
    401  return new Promise(resolve => {
    402    params.onNavComplete = resolve;
    403    doPageNavigation(params);
    404  });
    405 }
    406 
    407 /**
    408 * Allows a test to wait for page navigation events, and notify a
    409 * callback when they've all been received.  This works exactly the
    410 * same as doPageNavigation(), except that no navigation is initiated.
    411 */
    412 function waitForPageEvents(params) {
    413  params.waitForEventsOnly = true;
    414  doPageNavigation(params);
    415 }
    416 
    417 function promisePageEvents(params) {
    418  if (params.hasOwnProperty("onNavComplete")) {
    419    throw new Error(
    420      "Can't use a onNavComplete completion callback with promisePageEvents."
    421    );
    422  }
    423  return new Promise(resolve => {
    424    params.waitForEventsOnly = true;
    425    params.onNavComplete = resolve;
    426    doPageNavigation(params);
    427  });
    428 }
    429 
    430 /**
    431 * The event listener which listens for expectedEvents.
    432 */
    433 function pageEventListener(
    434  event,
    435  originalTargetIsHTMLDocument = HTMLDocument.isInstance(event.originalTarget)
    436 ) {
    437  try {
    438    dump(
    439      "TEST: eventListener received a " +
    440        event.type +
    441        " event for page " +
    442        event.originalTarget.title +
    443        ", persisted=" +
    444        event.persisted +
    445        "\n"
    446    );
    447  } catch (e) {
    448    // Ignore any exception.
    449  }
    450 
    451  // If this page shouldn't be in the bfcache because it was previously
    452  // loaded with .preventBFCache, make sure that its pageshow event
    453  // has .persisted = false, even if the test doesn't explicitly test
    454  // for .persisted.
    455  if (
    456    event.type == "pageshow" &&
    457    (gNavType == NAV_BACK ||
    458      gNavType == NAV_FORWARD ||
    459      gNavType == NAV_GOTOINDEX)
    460  ) {
    461    let uri = TestWindow.getBrowser().currentURI.spec;
    462    if (uri in gUrisNotInBFCache) {
    463      ok(
    464        !event.persisted,
    465        "pageshow event has .persisted = false, even " +
    466          "though it was loaded with .preventBFCache previously\n"
    467      );
    468    }
    469  }
    470 
    471  if (typeof gUnexpectedEvents != "undefined") {
    472    is(
    473      gUnexpectedEvents.indexOf(event.type),
    474      -1,
    475      "Should not get unexpected event " + event.type
    476    );
    477  }
    478 
    479  // If no expected events were specified, mark the final event as having been
    480  // triggered when a pageshow event is fired; this will allow
    481  // doPageNavigation() to return.
    482  if (typeof gExpectedEvents == "undefined" && event.type == "pageshow") {
    483    waitForNextPaint(function () {
    484      gFinalEvent = true;
    485    });
    486    return;
    487  }
    488 
    489  // If there are explicitly no expected events, but we receive one, it's an
    490  // error.
    491  if (!gExpectedEvents.length) {
    492    ok(false, "Unexpected event (" + event.type + ") occurred");
    493    return;
    494  }
    495 
    496  // Grab the next expected event, and compare its attributes against the
    497  // actual event.
    498  let expected = gExpectedEvents.shift();
    499 
    500  is(
    501    event.type,
    502    expected.type,
    503    "A " +
    504      expected.type +
    505      " event was expected, but a " +
    506      event.type +
    507      " event occurred"
    508  );
    509 
    510  if (typeof expected.title != "undefined") {
    511    ok(
    512      originalTargetIsHTMLDocument,
    513      "originalTarget for last " + event.type + " event not an HTMLDocument"
    514    );
    515    is(
    516      event.originalTarget.title,
    517      expected.title,
    518      "A " +
    519        event.type +
    520        " event was expected for page " +
    521        expected.title +
    522        ", but was fired for page " +
    523        event.originalTarget.title
    524    );
    525  }
    526 
    527  if (typeof expected.persisted != "undefined") {
    528    is(
    529      event.persisted,
    530      expected.persisted,
    531      "The persisted property of the " +
    532        event.type +
    533        " event on page " +
    534        event.originalTarget.location +
    535        " had an unexpected value"
    536    );
    537  }
    538 
    539  if ("visibilityState" in expected) {
    540    is(
    541      event.originalTarget.visibilityState,
    542      expected.visibilityState,
    543      "The visibilityState property of the document on page " +
    544        event.originalTarget.location +
    545        " had an unexpected value"
    546    );
    547  }
    548 
    549  if ("hidden" in expected) {
    550    is(
    551      event.originalTarget.hidden,
    552      expected.hidden,
    553      "The hidden property of the document on page " +
    554        event.originalTarget.location +
    555        " had an unexpected value"
    556    );
    557  }
    558 
    559  // If we're out of expected events, let doPageNavigation() return.
    560  if (!gExpectedEvents.length) {
    561    waitForNextPaint(function () {
    562      gFinalEvent = true;
    563    });
    564  }
    565 }
    566 
    567 DocShellHelpersParent.eventListener = pageEventListener;
    568 
    569 /**
    570 * End a test.
    571 */
    572 function finish() {
    573  // Work around bug 467960.
    574  let historyPurged;
    575  if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
    576    let history = TestWindow.getBrowser().browsingContext?.sessionHistory;
    577    history.purgeHistory(history.count);
    578    historyPurged = Promise.resolve();
    579  } else {
    580    historyPurged = SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
    581      let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory
    582        .legacySHistory;
    583      history.purgeHistory(history.count);
    584    });
    585  }
    586 
    587  // If the test changed the value of max_total_viewers via a call to
    588  // enableBFCache(), then restore it now.
    589  if (typeof gOrigMaxTotalViewers != "undefined") {
    590    Services.prefs.setIntPref(
    591      "browser.sessionhistory.max_total_viewers",
    592      gOrigMaxTotalViewers
    593    );
    594  }
    595 
    596  // Close the test window and signal the framework that the test is done.
    597  let opener = window.opener;
    598  let SimpleTest = opener.wrappedJSObject.SimpleTest;
    599 
    600  // Wait for the window to be closed before finishing the test
    601  Services.ww.registerNotification(function observer(subject, topic) {
    602    if (topic == "domwindowclosed") {
    603      Services.ww.unregisterNotification(observer);
    604      SimpleTest.waitForFocus(SimpleTest.finish, opener);
    605    }
    606  });
    607 
    608  historyPurged.then(_ => {
    609    window.close();
    610  });
    611 }
    612 
    613 /**
    614 * Helper function which waits until another function returns true, or until a
    615 * timeout occurs, and then notifies a callback.
    616 *
    617 * Parameters:
    618 *
    619 *    fn: a function which is evaluated repeatedly, and when it turns true,
    620 *        the onWaitComplete callback is notified.
    621 *
    622 *    onWaitComplete:  a callback which will be notified when fn() returns
    623 *        true, or when a timeout occurs.
    624 *
    625 *    timeout: a timeout, in seconds or ms, after which waitForTrue() will
    626 *        fail an assertion and then return, even if the fn function never
    627 *        returns true.  If timeout is undefined, waitForTrue() will never
    628 *        time out.
    629 */
    630 function waitForTrue(fn, onWaitComplete, timeout) {
    631  promiseTrue(fn, timeout).then(() => {
    632    onWaitComplete.call();
    633  });
    634 }
    635 
    636 function promiseTrue(fn, timeout) {
    637  if (typeof timeout != "undefined") {
    638    // If timeoutWait is less than 500, assume it represents seconds, and
    639    // convert to ms.
    640    if (timeout < 500) {
    641      timeout *= 1000;
    642    }
    643  }
    644 
    645  // Loop until the test function returns true, or until a timeout occurs,
    646  // if a timeout is defined.
    647  let intervalid, timeoutid;
    648  let condition = new Promise(resolve => {
    649    intervalid = setInterval(async () => {
    650      if (await fn.call()) {
    651        resolve();
    652      }
    653    }, 20);
    654  });
    655  if (typeof timeout != "undefined") {
    656    condition = Promise.race([
    657      condition,
    658      new Promise((_, reject) => {
    659        timeoutid = setTimeout(() => {
    660          reject();
    661        }, timeout);
    662      }),
    663    ]);
    664  }
    665  return condition
    666    .finally(() => {
    667      clearInterval(intervalid);
    668    })
    669    .then(() => {
    670      clearTimeout(timeoutid);
    671    });
    672 }
    673 
    674 function waitForNextPaint(cb) {
    675  requestAnimationFrame(_ => requestAnimationFrame(cb));
    676 }
    677 
    678 function promiseNextPaint() {
    679  return new Promise(resolve => {
    680    waitForNextPaint(resolve);
    681  });
    682 }
    683 
    684 /**
    685 * Enable or disable the bfcache.
    686 *
    687 * Parameters:
    688 *
    689 *   enable: if true, set max_total_viewers to -1 (the default); if false, set
    690 *           to 0 (disabled), if a number, set it to that specific number
    691 */
    692 function enableBFCache(enable) {
    693  // If this is the first time the test called enableBFCache(),
    694  // store the original value of max_total_viewers, so it can
    695  // be restored at the end of the test.
    696  if (typeof gOrigMaxTotalViewers == "undefined") {
    697    gOrigMaxTotalViewers = Services.prefs.getIntPref(
    698      "browser.sessionhistory.max_total_viewers"
    699    );
    700  }
    701 
    702  if (typeof enable == "boolean") {
    703    if (enable) {
    704      Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", -1);
    705    } else {
    706      Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0);
    707    }
    708  } else if (typeof enable == "number") {
    709    Services.prefs.setIntPref(
    710      "browser.sessionhistory.max_total_viewers",
    711      enable
    712    );
    713  }
    714 }
    715 
    716 /*
    717 * get http root for local tests.  Use a single extractJarToTmp instead of
    718 * extracting for each test.
    719 * Returns a file://path if we have a .jar file
    720 */
    721 function getHttpRoot() {
    722  var location = window.location.href;
    723  location = getRootDirectory(location);
    724  var jar = getJar(location);
    725  if (jar != null) {
    726    if (gExtractedPath == null) {
    727      var resolved = extractJarToTmp(jar);
    728      gExtractedPath = resolved.path;
    729    }
    730  } else {
    731    return null;
    732  }
    733  return "file://" + gExtractedPath + "/";
    734 }
    735 
    736 /**
    737 * Returns the full HTTP url for a file in the mochitest docshell test
    738 * directory.
    739 */
    740 function getHttpUrl(filename) {
    741  var root = getHttpRoot();
    742  if (root == null) {
    743    root = "http://mochi.test:8888/chrome/docshell/test/chrome/";
    744  }
    745  return root + filename;
    746 }
    747 
    748 /**
    749 * A convenience object with methods that return the current test window,
    750 * browser, and document.
    751 */
    752 var TestWindow = {};
    753 TestWindow.getWindow = function () {
    754  return document.getElementById("content").contentWindow;
    755 };
    756 TestWindow.getBrowser = function () {
    757  return document.getElementById("content");
    758 };
    759 TestWindow.getDocument = function () {
    760  return document.getElementById("content").contentDocument;
    761 };