Bug 1434376 - Add basic tests for window.promiseDocumentFlushed. r?paolo draft
authorMike Conley <mconley@mozilla.com>
Sun, 11 Feb 2018 20:13:53 -0500
changeset 759487 483a0cd846ded2ccf85aa3a433319309484185c2
parent 759486 d9c0a8de52cf4c3439cf93e8148274f1062a8bb2
child 759488 0308b3734ccf5ec44f1afb62880a0a6661040e90
push id100366
push usermconley@mozilla.com
push dateSun, 25 Feb 2018 01:20:16 +0000
reviewerspaolo
bugs1434376
milestone60.0a1
Bug 1434376 - Add basic tests for window.promiseDocumentFlushed. r?paolo MozReview-Commit-ID: KmyqaupJRtw
dom/base/test/browser.ini
dom/base/test/browser_promiseDocumentFlushed.js
--- a/dom/base/test/browser.ini
+++ b/dom/base/test/browser.ini
@@ -43,14 +43,15 @@ tags = mcb
 skip-if = !e10s # this only makes sense with e10s-multi
 [browser_messagemanager_loadprocessscript.js]
 [browser_aboutnewtab_process_selection.js]
 skip-if = !e10s # this only makes sense with e10s-multi
 [browser_messagemanager_targetframeloader.js]
 [browser_messagemanager_unload.js]
 [browser_pagehide_on_tab_close.js]
 skip-if = e10s # this tests non-e10s behavior. it's not expected to work in e10s.
+[browser_promiseDocumentFlushed.js]
 [browser_state_notifications.js]
 skip-if = true # Bug 1271028
 [browser_use_counters.js]
 [browser_timeout_throttling_with_audio_playback.js]
 [browser_bug1303838.js]
 [browser_inputStream_structuredClone.js]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/browser_promiseDocumentFlushed.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Dirties style and layout on the current browser window.
+ *
+ * @param {Number} Optional factor by which to modify the DOM. Useful for
+ *        when multiple calls to dirtyTheDOM may occur, and you need them
+ *        to dirty the DOM differently from one another. If you only need
+ *        to dirty the DOM once, this can be omitted.
+ */
+function dirtyStyleAndLayout(factor = 1) {
+  gNavToolbox.style.padding = factor + "px";
+}
+
+/**
+ * Dirties style of the current browser window, but NOT layout.
+ */
+function dirtyStyle() {
+  gNavToolbox.style.color = "red";
+}
+
+const gWindowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+
+/**
+ * Asserts that no style or layout flushes are required by the
+ * current window.
+ */
+function assertNoFlushesRequired() {
+  Assert.ok(!gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_STYLE),
+            "No flushes are required.");
+}
+
+/**
+ * Asserts that the DOM has been dirtied, and so style and layout flushes
+ * are required.
+ */
+function assertFlushesRequired() {
+  Assert.ok(gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_LAYOUT),
+            "Style and layout flushes are required.");
+}
+
+/**
+ * Removes style changes from dirtyTheDOM() from the browser window,
+ * and resolves once the refresh driver ticks.
+ */
+async function cleanTheDOM() {
+  gNavToolbox.style.padding = "";
+  gNavToolbox.style.color = "";
+  await window.promiseDocumentFlushed(() => {});
+}
+
+add_task(async function setup() {
+  registerCleanupFunction(cleanTheDOM);
+});
+
+/**
+ * Tests that if the DOM is dirty, that promiseDocumentFlushed will
+ * resolve once layout and style have been flushed.
+ */
+add_task(async function test_basic() {
+  dirtyStyleAndLayout();
+  assertFlushesRequired();
+
+  await window.promiseDocumentFlushed(() => {});
+  assertNoFlushesRequired();
+
+  dirtyStyle();
+  assertFlushesRequired();
+
+  await window.promiseDocumentFlushed(() => {});
+  assertNoFlushesRequired();
+
+  // The DOM should be clean already, but we'll do this anyway to isolate
+  // failures from other tests.
+  await cleanTheDOM();
+});
+
+/**
+ * Test that values returned by the callback passed to promiseDocumentFlushed
+ * get passed down through the Promise resolution.
+ */
+add_task(async function test_can_get_results_from_callback() {
+  const NEW_PADDING = "2px";
+
+  gNavToolbox.style.padding = NEW_PADDING;
+
+  assertFlushesRequired();
+
+  let paddings = await window.promiseDocumentFlushed(() => {
+    let style = window.getComputedStyle(gNavToolbox);
+    return {
+      left: style.paddingLeft,
+      right: style.paddingRight,
+      top: style.paddingTop,
+      bottom: style.paddingBottom,
+    };
+  });
+
+  for (let prop in paddings) {
+    Assert.equal(paddings[prop], NEW_PADDING,
+                 "Got expected padding");
+  }
+
+  await cleanTheDOM();
+
+  gNavToolbox.style.padding = NEW_PADDING;
+
+  assertFlushesRequired();
+
+  let rect = await window.promiseDocumentFlushed(() => {
+    let observer = {
+      reflow() {
+        Assert.ok(false, "A reflow should not have occurred.");
+      },
+      reflowInterruptible() {},
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
+                                             Ci.nsISupportsWeakReference])
+    };
+
+    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation)
+                         .QueryInterface(Ci.nsIDocShell);
+    docShell.addWeakReflowObserver(observer);
+
+    let toolboxRect = gNavToolbox.getBoundingClientRect();
+
+    docShell.removeWeakReflowObserver(observer);
+    return toolboxRect;
+  });
+
+  // The actual values of this rect aren't super important for
+  // the purposes of this test - we just want to know that a valid
+  // rect was returned, so checking for properties being greater than
+  // 0 is sufficient.
+  for (let property of ["width", "height"]) {
+    Assert.ok(rect[property] > 0, `Rect property ${property} > 0 (${rect[property]})`);
+  }
+
+  await cleanTheDOM();
+});
+
+/**
+ * Test that if promiseDocumentFlushed is requested on a window
+ * that closes before it gets a chance to do a refresh driver
+ * tick, the promiseDocumentFlushed Promise is still resolved, and
+ * the callback is still called.
+ */
+add_task(async function test_resolved_in_window_close() {
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+
+  await win.promiseDocumentFlushed(() => {});
+
+  let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDocShell);
+  docShell.contentViewer.pausePainting();
+
+  win.gNavToolbox.style.padding = "5px";
+
+  const EXPECTED = 1234;
+  let promise = win.promiseDocumentFlushed(() => {
+    // Despite the window not painting before closing, this
+    // callback should be fired when the window gets torn
+    // down and should stil be able to return a result.
+    return EXPECTED;
+  });
+
+  await BrowserTestUtils.closeWindow(win);
+  Assert.equal(await promise, EXPECTED);
+});
+
+/**
+ * Test that re-entering promiseDocumentFlushed is not possible
+ * from within a promiseDocumentFlushed callback. Doing so will
+ * result in the outer Promise rejecting with NS_ERROR_FAILURE.
+ */
+add_task(async function test_reentrancy() {
+  dirtyStyleAndLayout();
+  assertFlushesRequired();
+
+  let promise = window.promiseDocumentFlushed(() => {
+    return window.promiseDocumentFlushed(() => {
+      Assert.ok(false, "Should never run this.");
+    });
+  });
+
+  await Assert.rejects(promise, ex => ex.result == Cr.NS_ERROR_FAILURE);
+});
+
+/**
+ * Tests the expected execution order of a series of promiseDocumentFlushed
+ * calls, their callbacks, and the resolutions of their Promises.
+ *
+ * When multiple promiseDocumentFlushed callbacks are queued, the callbacks
+ * should always been run first before any of the Promises are resolved.
+ *
+ * The callbacks should run in the order that they were queued in via
+ * promiseDocumentFlushed. The Promise resolutions should similarly run
+ * in the order that promiseDocumentFlushed was called in.
+ */
+add_task(async function test_execution_order() {
+  let result = [];
+
+  dirtyStyleAndLayout(1);
+  let promise1 = window.promiseDocumentFlushed(() => {
+    result.push(0);
+  }).then(() => {
+    result.push(2);
+  });
+
+  let promise2 = window.promiseDocumentFlushed(() => {
+    result.push(1);
+  }).then(() => {
+    result.push(3);
+  });
+
+  await Promise.all([promise1, promise2]);
+
+  Assert.equal(result.length, 4,
+    "Should have run all callbacks and Promises.");
+
+  let promise3 = window.promiseDocumentFlushed(() => {
+    result.push(4);
+  }).then(() => {
+    result.push(6);
+  });
+
+  let promise4 = window.promiseDocumentFlushed(() => {
+    result.push(5);
+  }).then(() => {
+    result.push(7);
+  });
+
+  await Promise.all([promise3, promise4]);
+
+  Assert.equal(result.length, 8,
+    "Should have run all callbacks and Promises.");
+
+  for (let i = 0; i < result.length; ++i) {
+    Assert.equal(result[i], i,
+      "Callbacks and Promises should have run in the expected order.");
+  }
+});