diff options
Diffstat (limited to 'dom/base/test/browser_promiseDocumentFlushed.js')
-rw-r--r-- | dom/base/test/browser_promiseDocumentFlushed.js | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/dom/base/test/browser_promiseDocumentFlushed.js b/dom/base/test/browser_promiseDocumentFlushed.js new file mode 100644 index 0000000000..1df8f7af55 --- /dev/null +++ b/dom/base/test/browser_promiseDocumentFlushed.js @@ -0,0 +1,292 @@ +/* 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.windowUtils; + +/** + * Asserts that no style or layout flushes are required by the + * current window. + */ +function assertNoFlushesRequired() { + Assert.ok( + !gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_STYLE), + "No style flushes are required." + ); + Assert.ok( + !gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_LAYOUT), + "No layout 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_STYLE), + "Style flush required." + ); + Assert.ok( + gWindowUtils.needsFlush(Ci.nsIDOMWindowUtils.FLUSH_LAYOUT), + "Layout flush 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_setup(async function () { + 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() { + info("Dirtying style / layout"); + dirtyStyleAndLayout(); + assertFlushesRequired(); + + info("Waiting"); + await window.promiseDocumentFlushed(() => {}); + assertNoFlushesRequired(); + + info("Dirtying style"); + dirtyStyle(); + assertFlushesRequired(); + + info("Waiting"); + await window.promiseDocumentFlushed(() => {}); + assertNoFlushesRequired(); + + // The DOM should be clean already, but we'll do this anyway to isolate + // failures from other tests. + info("Cleaning up"); + 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: ChromeUtils.generateQI([ + "nsIReflowObserver", + "nsISupportsWeakReference", + ]), + }; + + let docShell = window.docShell; + 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(() => {}); + + // Use advanceTimeAndRefresh to pause paints in the window: + let utils = win.windowUtils; + utils.advanceTimeAndRefresh(0); + + 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 InvalidStateError. + */ +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.name == "InvalidStateError"); +}); + +/** + * 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." + ); + } + + await cleanTheDOM(); +}); + +/** + * Tests that modifying the DOM within a promiseDocumentFlushed callback + * will result in the Promise being rejected. + */ +add_task(async function test_reject_on_modification() { + dirtyStyleAndLayout(1); + assertFlushesRequired(); + + let promise = window.promiseDocumentFlushed(() => { + dirtyStyleAndLayout(2); + }); + + await Assert.rejects(promise, /NoModificationAllowedError/); + + await cleanTheDOM(); +}); |