/* 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(); });