Bug 1434376 - Add basic tests for window.promiseDocumentFlushed. r?paolo
MozReview-Commit-ID: KmyqaupJRtw
--- 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.");
+ }
+});