summaryrefslogtreecommitdiffstats
path: root/dom/base/test/browser_promiseDocumentFlushed.js
blob: 1df8f7af554287c3be89267a4e062bfa6daac88d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
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();
});