tor-browser

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

head.js (17233B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
      7  SpecialPowers.Ci.nsIGfxInfo
      8 );
      9 
     10 async function waitForBlockedPopups(numberOfPopups, { doc }) {
     11  let toolbarDoc = doc || document;
     12  let menupopup = toolbarDoc.getElementById("blockedPopupOptions");
     13  await BrowserTestUtils.waitForCondition(() => {
     14    let popups = menupopup.querySelectorAll("[popupReportIndex]");
     15    return popups.length == numberOfPopups;
     16  }, `Waiting for ${numberOfPopups} popups`);
     17 }
     18 
     19 /*
     20 * Tests that a sequence of size changes ultimately results in the latest
     21 * requested size. The test also fails when an unexpected window size is
     22 * observed in a resize event.
     23 *
     24 * aPropertyDeltas    List of objects where keys describe the name of a window
     25 *                    property and the values the difference to its initial
     26 *                    value.
     27 *
     28 * aInstant           Issue changes without additional waiting in between.
     29 *
     30 * A brief example of the resutling code that is effectively run for the
     31 * following list of deltas:
     32 * [{outerWidth: 5, outerHeight: 10}, {outerWidth: 10}]
     33 *
     34 * let initialWidth = win.outerWidth;
     35 * let initialHeight = win.outerHeight;
     36 *
     37 * if (aInstant) {
     38 *   win.outerWidth = initialWidth + 5;
     39 *   win.outerHeight = initialHeight + 10;
     40 *
     41 *   win.outerWidth = initialWidth + 10;
     42 * } else {
     43 *   win.requestAnimationFrame(() => {
     44 *     win.outerWidth = initialWidth + 5;
     45 *     win.outerHeight = initialHeight + 10;
     46 *
     47 *     win.requestAnimationFrame(() => {
     48 *        win.outerWidth = initialWidth + 10;
     49 *     });
     50 *   });
     51 * }
     52 */
     53 async function testPropertyDeltas(
     54  aPropertyDeltas,
     55  aInstant,
     56  aPropInfo,
     57  aMsg,
     58  aWaitForCompletion
     59 ) {
     60  let msg = `[${aMsg}]`;
     61 
     62  let win = this.content.popup || this.content.wrappedJSObject;
     63 
     64  // Property names and mapping from ResizeMoveTest
     65  let {
     66    sizeProps,
     67    positionProps /* can be empty/incomplete as workaround on Linux */,
     68    readonlyProps,
     69    crossBoundsMapping,
     70  } = aPropInfo;
     71 
     72  let stringifyState = state => {
     73    let stateMsg = sizeProps
     74      .concat(positionProps)
     75      .filter(prop => state[prop] !== undefined)
     76      .map(prop => `${prop}: ${state[prop]}`)
     77      .join(", ");
     78    return `{ ${stateMsg} }`;
     79  };
     80 
     81  let initialState = {};
     82  let finalState = {};
     83 
     84  info("Initializing all values to current state.");
     85  for (let prop of sizeProps.concat(positionProps)) {
     86    let value = win[prop];
     87    initialState[prop] = value;
     88    finalState[prop] = value;
     89  }
     90 
     91  // List of potential states during resize events. The current state is also
     92  // considered valid, as the resize event might still be outstanding.
     93  let validResizeStates = [initialState];
     94 
     95  let updateFinalState = (aProp, aDelta) => {
     96    if (
     97      readonlyProps.includes(aProp) ||
     98      !sizeProps.concat(positionProps).includes(aProp)
     99    ) {
    100      throw new Error(`Unexpected property "${aProp}".`);
    101    }
    102 
    103    // Update both properties of the same axis.
    104    let otherProp = crossBoundsMapping[aProp];
    105    finalState[aProp] = initialState[aProp] + aDelta;
    106    finalState[otherProp] = initialState[otherProp] + aDelta;
    107 
    108    // Mark size as valid in resize event.
    109    if (sizeProps.includes(aProp)) {
    110      let state = {};
    111      sizeProps.forEach(p => (state[p] = finalState[p]));
    112      validResizeStates.push(state);
    113    }
    114  };
    115 
    116  info("Adding resize event listener.");
    117  let resizeCount = 0;
    118  let resizeListener = evt => {
    119    resizeCount++;
    120 
    121    let currentSizeState = {};
    122    sizeProps.forEach(p => (currentSizeState[p] = win[p]));
    123 
    124    info(
    125      `${msg} ${resizeCount}. resize event: ${stringifyState(currentSizeState)}`
    126    );
    127    let matchingIndex = validResizeStates.findIndex(state =>
    128      sizeProps.every(p => state[p] == currentSizeState[p])
    129    );
    130    if (matchingIndex < 0) {
    131      info(`${msg} Size state should have been one of:`);
    132      for (let state of validResizeStates) {
    133        info(stringifyState(state));
    134      }
    135    }
    136 
    137    if (win.gBrowser && evt.target != win) {
    138      // Without e10s we receive content resize events in chrome windows.
    139      todo(false, `${msg} Resize event target is our window.`);
    140      return;
    141    }
    142 
    143    Assert.greaterOrEqual(
    144      matchingIndex,
    145      0,
    146      `${msg} Valid intermediate state. Current: ` +
    147        stringifyState(currentSizeState)
    148    );
    149 
    150    // No longer allow current and preceding states.
    151    validResizeStates.splice(0, matchingIndex + 1);
    152  };
    153  win.addEventListener("resize", resizeListener);
    154 
    155  info("Starting property changes.");
    156  await new Promise(resolve => {
    157    let index = 0;
    158    let next = async () => {
    159      let pre = `${msg} [${index + 1}/${aPropertyDeltas.length}]`;
    160 
    161      let deltaObj = aPropertyDeltas[index];
    162      for (let prop in deltaObj) {
    163        updateFinalState(prop, deltaObj[prop]);
    164 
    165        let targetValue = initialState[prop] + deltaObj[prop];
    166        info(`${pre} Setting ${prop} to ${targetValue}.`);
    167        if (sizeProps.includes(prop)) {
    168          win.resizeTo(finalState.outerWidth, finalState.outerHeight);
    169        } else {
    170          win.moveTo(finalState.screenX, finalState.screenY);
    171        }
    172        if (aWaitForCompletion) {
    173          await ContentTaskUtils.waitForCondition(
    174            () => win[prop] == targetValue,
    175            `${msg} Waiting for ${prop} to be ${targetValue}.`
    176          );
    177        }
    178      }
    179 
    180      index++;
    181      if (index < aPropertyDeltas.length) {
    182        scheduleNext();
    183      } else {
    184        resolve();
    185      }
    186    };
    187 
    188    let scheduleNext = () => {
    189      if (aInstant) {
    190        next();
    191      } else {
    192        info(`${msg} Requesting animation frame.`);
    193        win.requestAnimationFrame(next);
    194      }
    195    };
    196    scheduleNext();
    197  });
    198 
    199  try {
    200    info(`${msg} Waiting for window to match the final state.`);
    201    await ContentTaskUtils.waitForCondition(
    202      () => sizeProps.concat(positionProps).every(p => win[p] == finalState[p]),
    203      "Waiting for final state."
    204    );
    205  } catch (e) {}
    206 
    207  info(`${msg} Checking final state.`);
    208  info(`${msg} Exepected: ${stringifyState(finalState)}`);
    209  info(`${msg} Actual:    ${stringifyState(win)}`);
    210  for (let prop of sizeProps.concat(positionProps)) {
    211    is(win[prop], finalState[prop], `${msg} Expected final value for ${prop}`);
    212  }
    213 
    214  win.removeEventListener("resize", resizeListener);
    215 }
    216 
    217 function roundedCenter(aDimension, aOrigin) {
    218  let center = aOrigin + Math.floor(aDimension / 2);
    219  return center - (center % 100);
    220 }
    221 
    222 class ResizeMoveTest {
    223  static WindowWidth = 200;
    224  static WindowHeight = 200;
    225  static WindowLeft = roundedCenter(screen.availWidth - 200, screen.left);
    226  static WindowTop = roundedCenter(screen.availHeight - 200, screen.top);
    227 
    228  static PropInfo = {
    229    sizeProps: ["outerWidth", "outerHeight", "innerWidth", "innerHeight"],
    230    positionProps: [
    231      "screenX",
    232      "screenY",
    233      /* readonly */ "mozInnerScreenX",
    234      /* readonly */ "mozInnerScreenY",
    235    ],
    236    readonlyProps: ["mozInnerScreenX", "mozInnerScreenY"],
    237    crossAxisMapping: {
    238      outerWidth: "outerHeight",
    239      outerHeight: "outerWidth",
    240      innerWidth: "innerHeight",
    241      innerHeight: "innerWidth",
    242      screenX: "screenY",
    243      screenY: "screenX",
    244      mozInnerScreenX: "mozInnerScreenY",
    245      mozInnerScreenY: "mozInnerScreenX",
    246    },
    247    crossBoundsMapping: {
    248      outerWidth: "innerWidth",
    249      outerHeight: "innerHeight",
    250      innerWidth: "outerWidth",
    251      innerHeight: "outerHeight",
    252      screenX: "mozInnerScreenX",
    253      screenY: "mozInnerScreenY",
    254      mozInnerScreenX: "screenX",
    255      mozInnerScreenY: "screenY",
    256    },
    257  };
    258 
    259  constructor(
    260    aPropertyDeltas,
    261    aInstant = false,
    262    aMsg = "ResizeMoveTest",
    263    aWaitForCompletion = false
    264  ) {
    265    this.propertyDeltas = aPropertyDeltas;
    266    this.instant = aInstant;
    267    this.msg = aMsg;
    268    this.waitForCompletion = aWaitForCompletion;
    269 
    270    // Allows to ignore positions while testing.
    271    this.ignorePositions = false;
    272    // Allows to ignore only mozInnerScreenX/Y properties while testing.
    273    this.ignoreMozInnerScreen = false;
    274    // Allows to skip checking the restored position after testing.
    275    this.ignoreRestoredPosition = false;
    276 
    277    if (AppConstants.platform == "linux" && !SpecialPowers.isHeadless) {
    278      // We can occasionally start the test while nsWindow reports a wrong
    279      // client offset (gdk origin and root_origin are out of sync). This
    280      // results in false expectations for the final mozInnerScreenX/Y values.
    281      this.ignoreMozInnerScreen = !ResizeMoveTest.hasCleanUpTask;
    282 
    283      let { positionProps } = ResizeMoveTest.PropInfo;
    284      let resizeOnlyTest = aPropertyDeltas.every(deltaObj =>
    285        positionProps.every(prop => deltaObj[prop] === undefined)
    286      );
    287 
    288      let isWayland = gfxInfo.windowProtocol == "wayland";
    289      if (resizeOnlyTest && isWayland) {
    290        // On Wayland we can't move the window in general. The window also
    291        // doesn't necessarily open our specified position.
    292        this.ignoreRestoredPosition = true;
    293        // We can catch bad screenX/Y at the start of the first test in a
    294        // window.
    295        this.ignorePositions = !ResizeMoveTest.hasCleanUpTask;
    296      }
    297    }
    298 
    299    if (!ResizeMoveTest.hasCleanUpTask) {
    300      ResizeMoveTest.hasCleanUpTask = true;
    301      registerCleanupFunction(ResizeMoveTest.Cleanup);
    302    }
    303 
    304    add_task(async () => {
    305      let tab = await ResizeMoveTest.GetOrCreateTab();
    306      let browsingContext =
    307        await ResizeMoveTest.GetOrCreatePopupBrowsingContext();
    308      if (!browsingContext) {
    309        return;
    310      }
    311 
    312      info("=== Running in content. ===");
    313      await this.run(browsingContext, `${this.msg} (content)`);
    314      await this.restorePopupState(browsingContext);
    315 
    316      info("=== Running in chrome. ===");
    317      let popupChrome = browsingContext.topChromeWindow;
    318      await this.run(popupChrome.browsingContext, `${this.msg} (chrome)`);
    319      await this.restorePopupState(browsingContext);
    320 
    321      info("=== Running in opener. ===");
    322      await this.run(tab.linkedBrowser, `${this.msg} (opener)`);
    323      await this.restorePopupState(browsingContext);
    324    });
    325  }
    326 
    327  async run(aBrowsingContext, aMsg) {
    328    let testType = this.instant ? "instant" : "fanned out";
    329    let msg = `${aMsg} (${testType})`;
    330 
    331    let propInfo = {};
    332    for (let k in ResizeMoveTest.PropInfo) {
    333      propInfo[k] = ResizeMoveTest.PropInfo[k];
    334    }
    335    if (this.ignoreMozInnerScreen) {
    336      todo(false, `[${aMsg}] Shouldn't ignore mozInnerScreenX/Y.`);
    337      propInfo.positionProps = propInfo.positionProps.filter(
    338        prop => !["mozInnerScreenX", "mozInnerScreenY"].includes(prop)
    339      );
    340    }
    341    if (this.ignorePositions) {
    342      todo(false, `[${aMsg}] Shouldn't ignore position.`);
    343      propInfo.positionProps = [];
    344    }
    345 
    346    info(`${msg}: ` + JSON.stringify(this.propertyDeltas));
    347    await SpecialPowers.spawn(
    348      aBrowsingContext,
    349      [
    350        this.propertyDeltas,
    351        this.instant,
    352        propInfo,
    353        msg,
    354        this.waitForCompletion,
    355      ],
    356      testPropertyDeltas
    357    );
    358  }
    359 
    360  async restorePopupState(aBrowsingContext) {
    361    info("Restore popup state.");
    362 
    363    let { deltaWidth, deltaHeight } = await SpecialPowers.spawn(
    364      aBrowsingContext,
    365      [],
    366      () => {
    367        return {
    368          deltaWidth: this.content.outerWidth - this.content.innerWidth,
    369          deltaHeight: this.content.outerHeight - this.content.innerHeight,
    370        };
    371      }
    372    );
    373 
    374    let chromeWindow = aBrowsingContext.topChromeWindow;
    375    let {
    376      WindowLeft: left,
    377      WindowTop: top,
    378      WindowWidth: width,
    379      WindowHeight: height,
    380    } = ResizeMoveTest;
    381 
    382    chromeWindow.resizeTo(width + deltaWidth, height + deltaHeight);
    383    chromeWindow.moveTo(left, top);
    384 
    385    await SpecialPowers.spawn(
    386      aBrowsingContext,
    387      [left, top, width, height, this.ignoreRestoredPosition],
    388      async (aLeft, aTop, aWidth, aHeight, aIgnorePosition) => {
    389        let win = this.content.wrappedJSObject;
    390 
    391        info("Waiting for restored size.");
    392        await ContentTaskUtils.waitForCondition(
    393          () => win.innerWidth == aWidth && win.innerHeight === aHeight,
    394          "Waiting for restored size."
    395        );
    396        is(win.innerWidth, aWidth, "Restored width.");
    397        is(win.innerHeight, aHeight, "Restored height.");
    398 
    399        if (!aIgnorePosition) {
    400          info("Waiting for restored position.");
    401          await ContentTaskUtils.waitForCondition(
    402            () => win.screenX == aLeft && win.screenY === aTop,
    403            "Waiting for restored position."
    404          );
    405          is(win.screenX, aLeft, "Restored screenX.");
    406          is(win.screenY, aTop, "Restored screenY.");
    407        } else {
    408          todo(false, "Shouldn't ignore restored position.");
    409        }
    410      }
    411    );
    412  }
    413 
    414  static async GetOrCreateTab() {
    415    if (ResizeMoveTest.tab) {
    416      return ResizeMoveTest.tab;
    417    }
    418 
    419    info("Opening tab.");
    420    ResizeMoveTest.tab = await BrowserTestUtils.openNewForegroundTab(
    421      window.gBrowser,
    422      "https://example.net/browser/browser/base/content/test/popups/popup_blocker_a.html"
    423    );
    424    return ResizeMoveTest.tab;
    425  }
    426 
    427  static async GetOrCreatePopupBrowsingContext() {
    428    if (ResizeMoveTest.popupBrowsingContext) {
    429      if (!ResizeMoveTest.popupBrowsingContext.isActive) {
    430        return undefined;
    431      }
    432      return ResizeMoveTest.popupBrowsingContext;
    433    }
    434 
    435    let tab = await ResizeMoveTest.GetOrCreateTab();
    436    info("Opening popup.");
    437    ResizeMoveTest.popupBrowsingContext = await SpecialPowers.spawn(
    438      tab.linkedBrowser,
    439      [
    440        ResizeMoveTest.WindowWidth,
    441        ResizeMoveTest.WindowHeight,
    442        ResizeMoveTest.WindowLeft,
    443        ResizeMoveTest.WindowTop,
    444      ],
    445      async (aWidth, aHeight, aLeft, aTop) => {
    446        let win = this.content.open(
    447          this.content.document.location.href,
    448          "_blank",
    449          `left=${aLeft},top=${aTop},width=${aWidth},height=${aHeight}`
    450        );
    451        this.content.popup = win;
    452 
    453        await new Promise(r => (win.onload = r));
    454 
    455        return win.browsingContext;
    456      }
    457    );
    458 
    459    return ResizeMoveTest.popupBrowsingContext;
    460  }
    461 
    462  static async Cleanup() {
    463    let browsingContext = ResizeMoveTest.popupBrowsingContext;
    464    if (browsingContext) {
    465      await SpecialPowers.spawn(browsingContext, [], () => {
    466        this.content.close();
    467      });
    468      delete ResizeMoveTest.popupBrowsingContext;
    469    }
    470 
    471    let tab = ResizeMoveTest.tab;
    472    if (tab) {
    473      await BrowserTestUtils.removeTab(tab);
    474      delete ResizeMoveTest.tab;
    475    }
    476    ResizeMoveTest.hasCleanUpTask = false;
    477  }
    478 }
    479 
    480 function chaosRequestLongerTimeout(aDoRequest) {
    481  if (aDoRequest && parseInt(Services.env.get("MOZ_CHAOSMODE"), 16)) {
    482    requestLongerTimeout(2);
    483  }
    484 }
    485 
    486 function createGenericResizeTests(aFirstValue, aSecondValue, aInstant, aMsg) {
    487  // Runtime almost doubles in chaos mode on Mac.
    488  chaosRequestLongerTimeout(AppConstants.platform == "macosx");
    489 
    490  let { crossBoundsMapping, crossAxisMapping } = ResizeMoveTest.PropInfo;
    491 
    492  for (let prop of ["innerWidth", "outerHeight"]) {
    493    // Mixing inner and outer property.
    494    for (let secondProp of [prop, crossBoundsMapping[prop]]) {
    495      let first = {};
    496      first[prop] = aFirstValue;
    497      let second = {};
    498      second[secondProp] = aSecondValue;
    499      new ResizeMoveTest(
    500        [first, second],
    501        aInstant,
    502        `${aMsg} ${prop},${secondProp}`
    503      );
    504    }
    505  }
    506 
    507  for (let prop of ["innerHeight", "outerWidth"]) {
    508    let first = {};
    509    first[prop] = aFirstValue;
    510    let second = {};
    511    second[prop] = aSecondValue;
    512 
    513    // Setting property of other axis before/between two changes.
    514    let otherProps = [
    515      crossAxisMapping[prop],
    516      crossAxisMapping[crossBoundsMapping[prop]],
    517    ];
    518    for (let interferenceProp of otherProps) {
    519      let interference = {};
    520      interference[interferenceProp] = 20;
    521      new ResizeMoveTest(
    522        [first, interference, second],
    523        aInstant,
    524        `${aMsg} ${prop},${interferenceProp},${prop}`
    525      );
    526      new ResizeMoveTest(
    527        [interference, first, second],
    528        aInstant,
    529        `${aMsg} ${interferenceProp},${prop},${prop}`
    530      );
    531    }
    532  }
    533 }
    534 
    535 function createGenericMoveTests(aInstant, aMsg) {
    536  // Runtime almost doubles in chaos mode on Mac.
    537  chaosRequestLongerTimeout(AppConstants.platform == "macosx");
    538 
    539  let { crossAxisMapping } = ResizeMoveTest.PropInfo;
    540 
    541  for (let prop of ["screenX", "screenY"]) {
    542    for (let [v1, v2, msg] of [
    543      [9, 10, `${aMsg}`],
    544      [11, 11, `${aMsg} repeat`],
    545      [12, 0, `${aMsg} revert`],
    546    ]) {
    547      let first = {};
    548      first[prop] = v1;
    549      let second = {};
    550      second[prop] = v2;
    551      new ResizeMoveTest([first, second], aInstant, `${msg} ${prop},${prop}`);
    552 
    553      let interferenceProp = crossAxisMapping[prop];
    554      let interference = {};
    555      interference[interferenceProp] = 20;
    556      new ResizeMoveTest(
    557        [first, interference, second],
    558        aInstant,
    559        `${aMsg} ${prop},${interferenceProp},${prop}`
    560      );
    561      new ResizeMoveTest(
    562        [interference, first, second],
    563        aInstant,
    564        `${msg} ${interferenceProp},${prop},${prop}`
    565      );
    566    }
    567  }
    568 }