tor-browser

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

browser_promiseDocumentFlushed.js (7733B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /**
      7 * Dirties style and layout on the current browser window.
      8 *
      9 * @param {number} Optional factor by which to modify the DOM. Useful for
     10 *        when multiple calls to dirtyTheDOM may occur, and you need them
     11 *        to dirty the DOM differently from one another. If you only need
     12 *        to dirty the DOM once, this can be omitted.
     13 */
     14 function dirtyStyleAndLayout(factor = 1) {
     15  gNavToolbox.style.padding = factor + "px";
     16 }
     17 
     18 /**
     19 * Dirties style of the current browser window, but NOT layout.
     20 */
     21 function dirtyStyle() {
     22  gNavToolbox.style.color = "red";
     23 }
     24 
     25 const gWindowUtils = window.windowUtils;
     26 
     27 /**
     28 * Asserts that no style or layout flushes are required by the
     29 * current window.
     30 */
     31 function assertNoFlushesRequired() {
     32  Assert.ok(
     33    !gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_STYLE),
     34    "No style flushes are required."
     35  );
     36  Assert.ok(
     37    !gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_LAYOUT),
     38    "No layout flushes are required."
     39  );
     40 }
     41 
     42 /**
     43 * Asserts that the DOM has been dirtied, and so style and layout flushes
     44 * are required.
     45 */
     46 function assertFlushesRequired() {
     47  Assert.ok(
     48    gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_STYLE),
     49    "Style flush required."
     50  );
     51  Assert.ok(
     52    gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_LAYOUT),
     53    "Layout flush required."
     54  );
     55 }
     56 
     57 /**
     58 * Removes style changes from dirtyTheDOM() from the browser window,
     59 * and resolves once the refresh driver ticks.
     60 */
     61 async function cleanTheDOM() {
     62  gNavToolbox.style.padding = "";
     63  gNavToolbox.style.color = "";
     64  await window.promiseDocumentFlushed(() => {});
     65 }
     66 
     67 add_setup(async function () {
     68  registerCleanupFunction(cleanTheDOM);
     69 });
     70 
     71 /**
     72 * Tests that if the DOM is dirty, that promiseDocumentFlushed will
     73 * resolve once layout and style have been flushed.
     74 */
     75 add_task(async function test_basic() {
     76  info("Dirtying style / layout");
     77  dirtyStyleAndLayout();
     78  assertFlushesRequired();
     79 
     80  info("Waiting");
     81  await window.promiseDocumentFlushed(() => {});
     82  assertNoFlushesRequired();
     83 
     84  info("Dirtying style");
     85  dirtyStyle();
     86  assertFlushesRequired();
     87 
     88  info("Waiting");
     89  await window.promiseDocumentFlushed(() => {});
     90  assertNoFlushesRequired();
     91 
     92  // The DOM should be clean already, but we'll do this anyway to isolate
     93  // failures from other tests.
     94  info("Cleaning up");
     95  await cleanTheDOM();
     96 });
     97 
     98 /**
     99 * Test that values returned by the callback passed to promiseDocumentFlushed
    100 * get passed down through the Promise resolution.
    101 */
    102 add_task(async function test_can_get_results_from_callback() {
    103  const NEW_PADDING = "2px";
    104 
    105  gNavToolbox.style.padding = NEW_PADDING;
    106 
    107  assertFlushesRequired();
    108 
    109  let paddings = await window.promiseDocumentFlushed(() => {
    110    let style = window.getComputedStyle(gNavToolbox);
    111    return {
    112      left: style.paddingLeft,
    113      right: style.paddingRight,
    114      top: style.paddingTop,
    115      bottom: style.paddingBottom,
    116    };
    117  });
    118 
    119  for (let prop in paddings) {
    120    Assert.equal(paddings[prop], NEW_PADDING, "Got expected padding");
    121  }
    122 
    123  await cleanTheDOM();
    124 
    125  gNavToolbox.style.padding = NEW_PADDING;
    126 
    127  assertFlushesRequired();
    128 
    129  let rect = await window.promiseDocumentFlushed(() => {
    130    let observer = {
    131      reflow() {
    132        Assert.ok(false, "A reflow should not have occurred.");
    133      },
    134      reflowInterruptible() {},
    135      QueryInterface: ChromeUtils.generateQI([
    136        "nsIReflowObserver",
    137        "nsISupportsWeakReference",
    138      ]),
    139    };
    140 
    141    let docShell = window.docShell;
    142    docShell.addWeakReflowObserver(observer);
    143 
    144    let toolboxRect = gNavToolbox.getBoundingClientRect();
    145 
    146    docShell.removeWeakReflowObserver(observer);
    147    return toolboxRect;
    148  });
    149 
    150  // The actual values of this rect aren't super important for
    151  // the purposes of this test - we just want to know that a valid
    152  // rect was returned, so checking for properties being greater than
    153  // 0 is sufficient.
    154  for (let property of ["width", "height"]) {
    155    Assert.greater(
    156      rect[property],
    157      0,
    158      `Rect property ${property} > 0 (${rect[property]})`
    159    );
    160  }
    161 
    162  await cleanTheDOM();
    163 });
    164 
    165 /**
    166 * Test that if promiseDocumentFlushed is requested on a window
    167 * that closes before it gets a chance to do a refresh driver
    168 * tick, the promiseDocumentFlushed Promise is still resolved, and
    169 * the callback is still called.
    170 */
    171 add_task(async function test_resolved_in_window_close() {
    172  let win = await BrowserTestUtils.openNewBrowserWindow();
    173 
    174  await win.promiseDocumentFlushed(() => {});
    175 
    176  // Use advanceTimeAndRefresh to pause paints in the window:
    177  let utils = win.windowUtils;
    178  utils.advanceTimeAndRefresh(0);
    179 
    180  win.gNavToolbox.style.padding = "5px";
    181 
    182  const EXPECTED = 1234;
    183  let promise = win.promiseDocumentFlushed(() => {
    184    // Despite the window not painting before closing, this
    185    // callback should be fired when the window gets torn
    186    // down and should stil be able to return a result.
    187    return EXPECTED;
    188  });
    189 
    190  await BrowserTestUtils.closeWindow(win);
    191  Assert.equal(await promise, EXPECTED);
    192 });
    193 
    194 /**
    195 * Test that re-entering promiseDocumentFlushed is not possible
    196 * from within a promiseDocumentFlushed callback. Doing so will
    197 * result in the outer Promise rejecting with InvalidStateError.
    198 */
    199 add_task(async function test_reentrancy() {
    200  dirtyStyleAndLayout();
    201  assertFlushesRequired();
    202 
    203  let promise = window.promiseDocumentFlushed(() => {
    204    return window.promiseDocumentFlushed(() => {
    205      Assert.ok(false, "Should never run this.");
    206    });
    207  });
    208 
    209  await Assert.rejects(promise, ex => ex.name == "InvalidStateError");
    210 });
    211 
    212 /**
    213 * Tests the expected execution order of a series of promiseDocumentFlushed
    214 * calls, their callbacks, and the resolutions of their Promises.
    215 *
    216 * When multiple promiseDocumentFlushed callbacks are queued, the callbacks
    217 * should always been run first before any of the Promises are resolved.
    218 *
    219 * The callbacks should run in the order that they were queued in via
    220 * promiseDocumentFlushed. The Promise resolutions should similarly run
    221 * in the order that promiseDocumentFlushed was called in.
    222 */
    223 add_task(async function test_execution_order() {
    224  let result = [];
    225 
    226  dirtyStyleAndLayout(1);
    227  let promise1 = window
    228    .promiseDocumentFlushed(() => {
    229      result.push(0);
    230    })
    231    .then(() => {
    232      result.push(2);
    233    });
    234 
    235  let promise2 = window
    236    .promiseDocumentFlushed(() => {
    237      result.push(1);
    238    })
    239    .then(() => {
    240      result.push(3);
    241    });
    242 
    243  await Promise.all([promise1, promise2]);
    244 
    245  Assert.equal(result.length, 4, "Should have run all callbacks and Promises.");
    246 
    247  let promise3 = window
    248    .promiseDocumentFlushed(() => {
    249      result.push(4);
    250    })
    251    .then(() => {
    252      result.push(6);
    253    });
    254 
    255  let promise4 = window
    256    .promiseDocumentFlushed(() => {
    257      result.push(5);
    258    })
    259    .then(() => {
    260      result.push(7);
    261    });
    262 
    263  await Promise.all([promise3, promise4]);
    264 
    265  Assert.equal(result.length, 8, "Should have run all callbacks and Promises.");
    266 
    267  for (let i = 0; i < result.length; ++i) {
    268    Assert.equal(
    269      result[i],
    270      i,
    271      "Callbacks and Promises should have run in the expected order."
    272    );
    273  }
    274 
    275  await cleanTheDOM();
    276 });
    277 
    278 /**
    279 * Tests that modifying the DOM within a promiseDocumentFlushed callback
    280 * will result in the Promise being rejected.
    281 */
    282 add_task(async function test_reject_on_modification() {
    283  dirtyStyleAndLayout(1);
    284  assertFlushesRequired();
    285 
    286  let promise = window.promiseDocumentFlushed(() => {
    287    dirtyStyleAndLayout(2);
    288  });
    289 
    290  await Assert.rejects(promise, /NoModificationAllowedError/);
    291 
    292  await cleanTheDOM();
    293 });