Bug 1407366 - Part 4: Adding a test case for testing letterboxing. r=johannh
authorTim Huang <tihuang@mozilla.com>
Sun, 03 Feb 2019 14:20:18 -0600
changeset 460191 9ec370629b85974ca63754fbc101b049e97cf831
parent 460190 1490c3e6cef1f187e96f464796ad1481f3259094
child 460192 146c1c01d9dd6eb806426a866a9b130c203354dc
push id35585
push useropoprus@mozilla.com
push dateThu, 21 Feb 2019 09:31:43 +0000
treeherdermozilla-central@2acb22602c60 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjohannh
bugs1407366
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1407366 - Part 4: Adding a test case for testing letterboxing. r=johannh This patch adds a test for ensuring the letterboxing works as we expect. It will open a tab and resize its window into several different sizes and to see if the margins are correctly apply. And it will also check that no margin should apply to a tab with chrome privilege.
browser/components/resistfingerprinting/test/browser/browser.ini
browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js
modules/libpref/init/all.js
toolkit/components/resistfingerprinting/RFPHelper.jsm
--- a/browser/components/resistfingerprinting/test/browser/browser.ini
+++ b/browser/components/resistfingerprinting/test/browser/browser.ini
@@ -6,16 +6,17 @@ support-files =
   file_keyBoardEvent.sjs
   file_navigator.html
   file_navigatorWorker.js
   file_workerNetInfo.js
   file_workerPerformance.js
   head.js
 
 [browser_block_mozAddonManager.js]
+[browser_dynamical_window_rounding.js]
 [browser_navigator.js]
 [browser_netInfo.js]
 [browser_performanceAPI.js]
 [browser_roundedWindow_dialogWindow.js]
 [browser_roundedWindow_newWindow.js]
 [browser_roundedWindow_open_max_inner.js]
 [browser_roundedWindow_open_max_outer.js]
 [browser_roundedWindow_open_mid_inner.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Bug 1407366 - A test case for reassuring the size of the content viewport is
+ *   rounded if the window is resized when letterboxing is enabled.
+ */
+
+const TEST_PATH = "http://example.net/browser/browser/components/resistfingerprinting/test/browser/";
+
+const DEFAULT_ROUNDED_WIDTH_STEP  = 200;
+const DEFAULT_ROUNDED_HEIGHT_STEP = 100;
+
+// A set of test cases which defines the width and the height of the outer window.
+const TEST_CASES = [
+  {width: 1250, height: 1000},
+  {width: 1500, height: 1050},
+  {width: 1120, height: 760},
+  {width: 800,  height: 600},
+  {width: 640,  height: 400},
+  {width: 500,  height: 350},
+  {width: 300,  height: 170},
+];
+
+function getPlatform() {
+  const {OS} = Services.appinfo;
+  if (OS == "WINNT") {
+    return "win";
+  } else if (OS == "Darwin") {
+    return "mac";
+  }
+  return "linux";
+}
+
+function handleOSFuzziness(aContent, aTarget) {
+  /*
+   * On Windows, we observed off-by-one pixel differences that
+   * couldn't be expained. When manually setting the window size
+   * to try to reproduce it; it did not occur.
+   */
+  if (getPlatform() == "win") {
+    return Math.abs(aContent - aTarget) <= 1;
+  }
+  return aContent == aTarget;
+}
+
+function checkForDefaultSetting(
+  aContentWidth, aContentHeight, aRealWidth, aRealHeight) {
+  // The default behavior for rounding is to round window with 200x100 stepping.
+  // So, we can get the rounded size by subtracting the remainder.
+  let targetWidth = aRealWidth - (aRealWidth % DEFAULT_ROUNDED_WIDTH_STEP);
+  let targetHeight = aRealHeight - (aRealHeight % DEFAULT_ROUNDED_HEIGHT_STEP);
+
+  // This platform-specific code is explained in the large comment below.
+  if (getPlatform() != "linux") {
+    ok(handleOSFuzziness(aContentWidth, targetWidth),
+      `Default Dimensions: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetWidth}px`);
+
+    ok(handleOSFuzziness(aContentHeight, targetHeight),
+      `Default Dimensions: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetHeight}px`);
+
+    // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends
+    return true;
+  }
+  // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds.
+  return handleOSFuzziness(aContentWidth, targetWidth) && handleOSFuzziness(aContentHeight, targetHeight);
+}
+
+async function test_dynamical_window_rounding(aWindow, aCheckFunc) {
+  // We need to wait for the updating the margins for the newly opened tab, or
+  // it will affect the following tests.
+  let promiseForTheFirstRounding =
+    TestUtils.topicObserved("test:letterboxing:update-margin-finish");
+
+  info("Open a content tab for testing.");
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    aWindow.gBrowser, TEST_PATH + "file_dummy.html");
+
+  info("Wait until the margins are applied for the opened tab.");
+  await promiseForTheFirstRounding;
+
+  let getContainerSize = (aTab) => {
+    let browserContainer = aWindow.gBrowser
+                                  .getBrowserContainer(aTab.linkedBrowser);
+    return {
+      containerWidth: browserContainer.clientWidth,
+      containerHeight: browserContainer.clientHeight,
+    };
+  };
+
+  for (let {width, height} of TEST_CASES) {
+    let caseString = "Case " + width + "x" + height + ": ";
+    // Create a promise for waiting for the margin update.
+    let promiseRounding =
+      TestUtils.topicObserved("test:letterboxing:update-margin-finish");
+
+    let {containerWidth, containerHeight} = getContainerSize(tab);
+
+    info(caseString + "Resize the window and wait until resize event happened (currently " +
+      containerWidth + "x" + containerHeight + ")");
+    await new Promise(resolve => {
+      ({containerWidth, containerHeight} = getContainerSize(tab));
+      info(caseString + "Resizing (currently " + containerWidth + "x" + containerHeight + ")");
+
+      aWindow.onresize = () => {
+        ({containerWidth, containerHeight} = getContainerSize(tab));
+        info(caseString + "Resized (currently " + containerWidth + "x" + containerHeight + ")");
+        if (getPlatform() == "linux" && containerWidth != width) {
+          /*
+           * We observed frequent test failures that resulted from receiving an onresize
+           * event where the browser was resized to an earlier requested dimension. This
+           * resize event happens on Linux only, and is an artifact of the asynchronous
+           * resizing. (See more discussion on 1407366#53)
+           *
+           * We cope with this problem in two ways.
+           *
+           * 1: If we detect that the browser was resized to the wrong value; we
+           *    redo the resize. (This is the lines of code immediately following this
+           *    comment)
+           * 2: We repeat the test until it works using waitForCondition(). But we still
+           *    test Win/Mac more thoroughly: they do not loop in waitForCondition more
+           *    than once, and can fail the test on the first attempt (because their
+           *    check() functions use ok() while on Linux, we do not all ok() and instead
+           *    rely on waitForCondition to fail).
+           *
+           * The logging statements in this test, and RFPHelper.jsm, help narrow down and
+           * illustrate the issue.
+           */
+          info(caseString + "We hit the weird resize bug. Resize it again.");
+          aWindow.resizeTo(width, height);
+        } else {
+          resolve();
+        }
+      };
+      aWindow.resizeTo(width, height);
+    });
+
+    ({containerWidth, containerHeight} = getContainerSize(tab));
+    info(caseString + "Waiting until margin has been updated on browser element. (currently " +
+      containerWidth + "x" + containerHeight + ")");
+    await promiseRounding;
+
+    info(caseString + "Get innerWidth/Height from the content.");
+    await BrowserTestUtils.waitForCondition(async () => {
+      let {contentWidth, contentHeight} = await ContentTask.spawn(
+        tab.linkedBrowser, null, () => {
+          return {
+            contentWidth: content.innerWidth,
+            contentHeight: content.innerHeight,
+          };
+        });
+
+      info(caseString + "Check the result.");
+      return aCheckFunc(contentWidth, contentHeight, containerWidth, containerHeight);
+    }, "Default Dimensions: The content window width is correctly rounded into.");
+  }
+
+  BrowserTestUtils.removeTab(tab);
+}
+
+async function test_customize_width_and_height(aWindow) {
+  const test_dimensions = `120x80, 200x143, 335x255, 600x312, 742x447, 813x558,
+                           990x672, 1200x733, 1470x858`;
+
+  await SpecialPowers.pushPrefEnv({"set":
+    [
+      ["privacy.resistFingerprinting.letterboxing.dimensions", test_dimensions],
+    ],
+  });
+
+  let dimensions_set = test_dimensions.split(",").map(item => {
+    let sizes = item.split("x").map(size => parseInt(size, 10));
+
+    return {
+      width: sizes[0],
+      height: sizes[1],
+    };
+  });
+
+  let checkDimension =
+    (aContentWidth, aContentHeight, aRealWidth, aRealHeight) => {
+      let matchingArea = aRealWidth * aRealHeight;
+      let minWaste = Number.MAX_SAFE_INTEGER;
+      let targetDimensions = undefined;
+
+      // Find the dimensions which waste the least content area.
+      for (let dim of dimensions_set) {
+        if (dim.width > aRealWidth || dim.height > aRealHeight) {
+          continue;
+        }
+
+        let waste = matchingArea - dim.width * dim.height;
+
+        if (waste >= 0 && waste < minWaste) {
+          targetDimensions = dim;
+          minWaste = waste;
+        }
+      }
+
+      // This platform-specific code is explained in the large comment above.
+      if (getPlatform() != "linux") {
+        ok(handleOSFuzziness(aContentWidth, targetDimensions.width),
+          `Custom Dimension: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetDimensions.width}`);
+
+        ok(handleOSFuzziness(aContentHeight, targetDimensions.height),
+          `Custom Dimension: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetDimensions.height}`);
+
+        // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends
+        return true;
+      }
+      // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds.
+      return handleOSFuzziness(aContentWidth, targetDimensions.width) && handleOSFuzziness(aContentHeight, targetDimensions.height);
+    };
+
+  await test_dynamical_window_rounding(aWindow, checkDimension);
+
+  await SpecialPowers.popPrefEnv();
+}
+
+async function test_no_rounding_for_chrome(aWindow) {
+  // First, resize the window to a size with is not rounded.
+  await new Promise(resolve => {
+    aWindow.onresize = () => resolve();
+    aWindow.resizeTo(700, 450);
+  });
+
+  // open a chrome privilege tab, like about:config.
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    aWindow.gBrowser, "about:config");
+
+  // Check that the browser element should not have a margin.
+  is(tab.linkedBrowser.style.margin, "", "There is no margin around chrome tab.");
+
+  BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({"set":
+    [
+      ["privacy.resistFingerprinting.letterboxing", true],
+      ["privacy.resistFingerprinting.letterboxing.testing", true],
+    ],
+  });
+});
+
+add_task(async function do_tests() {
+  // Store the original window size before testing.
+  let originalOuterWidth = window.outerWidth;
+  let originalOuterHeight = window.outerHeight;
+
+  info("Run test for the default window rounding.");
+  await test_dynamical_window_rounding(window, checkForDefaultSetting);
+
+  info("Run test for the window rounding with customized dimensions.");
+  await test_customize_width_and_height(window);
+
+  info("Run test for no margin around tab with the chrome privilege.");
+  await test_no_rounding_for_chrome(window);
+
+  // Restore the original window size.
+  window.outerWidth = originalOuterWidth;
+  window.outerHeight = originalOuterHeight;
+
+  // Testing that whether the dynamical rounding works for new windows.
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+
+  info("Run test for the default window rounding in new window.");
+  await test_dynamical_window_rounding(win, checkForDefaultSetting);
+
+  info("Run test for the window rounding with customized dimensions in new window.");
+  await test_customize_width_and_height(win);
+
+  info("Run test for no margin around tab with the chrome privilege in new window.");
+  await test_no_rounding_for_chrome(win);
+
+  await BrowserTestUtils.closeWindow(win);
+});
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1437,16 +1437,19 @@ pref("privacy.firstparty.isolate",      
 // This pref is effective only when "privacy.firstparty.isolate" is true.
 pref("privacy.firstparty.isolate.restrict_opener_access", true);
 // We automatically decline canvas permission requests if they are not initiated
 // from user input. Just in case that breaks something, we allow the user to revert
 // this behavior with this obscure pref. We do not intend to support this long term.
 // If you do set it, to work around some broken website, please file a bug with
 // information so we can understand why it is needed.
 pref("privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts", true);
+// The log level for browser console messages logged in RFPHelper.jsm
+// Change to 'All' and restart to see the messages
+pref("privacy.resistFingerprinting.jsmloglevel", "Warn");
 // A subset of Resist Fingerprinting protections focused specifically on timers for testing
 // This affects the Animation API, the performance APIs, Date.getTime, Event.timestamp,
 //   File.lastModified, audioContext.currentTime, canvas.captureStream.currentTime
 pref("privacy.reduceTimerPrecision", true);
 // Dynamically tune the resolution of the timer reduction for both of the two above prefs
 pref("privacy.resistFingerprinting.reduceTimerPrecision.microseconds", 1000);
 // Enable jittering the clock one precision value forward
 pref("privacy.resistFingerprinting.reduceTimerPrecision.jitter", true);
--- a/toolkit/components/resistfingerprinting/RFPHelper.jsm
+++ b/toolkit/components/resistfingerprinting/RFPHelper.jsm
@@ -11,22 +11,36 @@ const {XPCOMUtils} = ChromeUtils.import(
 
 const kPrefResistFingerprinting = "privacy.resistFingerprinting";
 const kPrefSpoofEnglish = "privacy.spoof_english";
 const kTopicHttpOnModifyRequest = "http-on-modify-request";
 
 const kPrefLetterboxing = "privacy.resistFingerprinting.letterboxing";
 const kPrefLetterboxingDimensions =
   "privacy.resistFingerprinting.letterboxing.dimensions";
+const kPrefLetterboxingTesting =
+  "privacy.resistFingerprinting.letterboxing.testing";
 const kTopicDOMWindowOpened = "domwindowopened";
 const kEventLetterboxingSizeUpdate = "Letterboxing:ContentSizeUpdated";
 
 const kDefaultWidthStepping = 200;
 const kDefaultHeightStepping = 100;
 
+var logConsole;
+function log(msg) {
+  if (!logConsole) {
+    logConsole = console.createInstance({
+      prefix: "RFPHelper.jsm",
+      maxLogLevelPref: "privacy.resistFingerprinting.jsmloglevel",
+    });
+  }
+
+  logConsole.log(msg);
+}
+
 class _RFPHelper {
   // ============================================================================
   // Shared Setup
   // ============================================================================
   constructor() {
     this._initialized = false;
   }
 
@@ -36,16 +50,18 @@ class _RFPHelper {
     }
     this._initialized = true;
 
     // Add unconditional observers
     Services.prefs.addObserver(kPrefResistFingerprinting, this);
     Services.prefs.addObserver(kPrefLetterboxing, this);
     XPCOMUtils.defineLazyPreferenceGetter(this, "_letterboxingDimensions",
       kPrefLetterboxingDimensions, "", null, this._parseLetterboxingDimensions);
+    XPCOMUtils.defineLazyPreferenceGetter(this, "_isLetterboxingTesting",
+      kPrefLetterboxingTesting, false);
 
     // Add RFP and Letterboxing observers if prefs are enabled
     this._handleResistFingerprintingChanged();
     this._handleLetterboxingPrefChanged();
   }
 
   uninit() {
     if (!this._initialized) {
@@ -321,16 +337,18 @@ class _RFPHelper {
     }
   }
 
   /**
    * The function will round the given browser by adding margins around the
    * content viewport.
    */
   async _roundContentView(aBrowser) {
+    let logId = Math.random();
+    log("_roundContentView[" + logId + "]");
     let win = aBrowser.ownerGlobal;
     let browserContainer = aBrowser.getTabBrowser()
                                    .getBrowserContainer(aBrowser);
 
     let {contentWidth, contentHeight, containerWidth, containerHeight} =
       await win.promiseDocumentFlushed(() => {
         let contentWidth = aBrowser.clientWidth;
         let contentHeight = aBrowser.clientHeight;
@@ -340,24 +358,31 @@ class _RFPHelper {
         return {
           contentWidth,
           contentHeight,
           containerWidth,
           containerHeight,
         };
       });
 
+    log("_roundContentView[" + logId + "] contentWidth=" + contentWidth + " contentHeight=" + contentHeight +
+      " containerWidth=" + containerWidth + " containerHeight=" + containerHeight + " ");
+
     let calcMargins = (aWidth, aHeight) => {
+      let result;
+      log("_roundContentView[" + logId + "] calcMargins(" + aWidth + ", " + aHeight + ")");
       // If the set is empty, we will round the content with the default
       // stepping size.
       if (!this._letterboxingDimensions.length) {
-        return {
+        result = {
           width: (aWidth % kDefaultWidthStepping) / 2,
           height: (aHeight % kDefaultHeightStepping) / 2,
         };
+        log("_roundContentView[" + logId + "] calcMargins(" + aWidth + ", " + aHeight + ") = " + result.width + " x " + result.height);
+        return result;
       }
 
       let matchingArea = aWidth * aHeight;
       let minWaste = Number.MAX_SAFE_INTEGER;
       let targetDimensions = undefined;
 
       // Find the desired dimensions which waste the least content area.
       for (let dim of this._letterboxingDimensions) {
@@ -370,46 +395,52 @@ class _RFPHelper {
         let waste = matchingArea - dim.width * dim.height;
 
         if (waste >= 0 && waste < minWaste) {
           targetDimensions = dim;
           minWaste = waste;
         }
       }
 
-      let result;
       // If we cannot find any dimensions match to the real content window, this
       // means the content area is smaller the smallest size in the set. In this
       // case, we won't apply any margins.
       if (!targetDimensions) {
         result = {
           width: 0,
           height: 0,
         };
       } else {
         result = {
           width: (aWidth - targetDimensions.width) / 2,
           height: (aHeight - targetDimensions.height) / 2,
         };
       }
 
+      log("_roundContentView[" + logId + "] calcMargins(" + aWidth + ", " + aHeight + ") = " + result.width + " x " + result.height);
       return result;
     };
 
     // Calculating the margins around the browser element in order to round the
     // content viewport. We will use a 200x100 stepping if the dimension set
     // is not given.
     let margins = calcMargins(containerWidth, containerHeight);
 
     // If the size of the content is already quantized, we do nothing.
     if (aBrowser.style.margin == `${margins.height}px ${margins.width}px`) {
+      log("_roundContentView[" + logId + "] is_rounded == true");
+      if (this._isLetterboxingTesting) {
+        log("_roundContentView[" + logId + "] is_rounded == true test:letterboxing:update-margin-finish");
+        Services.obs.notifyObservers(null, "test:letterboxing:update-margin-finish");
+      }
       return;
     }
 
     win.requestAnimationFrame(() => {
+      log("_roundContentView[" + logId + "] setting margins to " + margins.width + " x " + margins.height);
       // One cannot (easily) control the color of a margin unfortunately.
       // An initial attempt to use a border instead of a margin resulted
       // in offset event dispatching; so for now we use a colorless margin.
       aBrowser.style.margin = `${margins.height}px ${margins.width}px`;
     });
   }
 
   _clearContentViewMargin(aBrowser) {