MarionetteReftestChild.sys.mjs (8461B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 setTimeout: "resource://gre/modules/Timer.sys.mjs", 9 10 Log: "chrome://remote/content/shared/Log.sys.mjs", 11 }); 12 13 ChromeUtils.defineLazyGetter(lazy, "logger", () => 14 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 15 ); 16 17 /** 18 * Child JSWindowActor to handle navigation for reftests relying on marionette. 19 */ 20 export class MarionetteReftestChild extends JSWindowActorChild { 21 constructor() { 22 super(); 23 24 // This promise will resolve with the URL recorded in the "load" event 25 // handler. This URL will not be impacted by any hash modification that 26 // might be performed by the test script. 27 // The harness should be loaded before loading any test page, so the actors 28 // should be registered before the "load" event is received for a test page. 29 this._loadedURLPromise = new Promise( 30 r => (this._resolveLoadedURLPromise = r) 31 ); 32 } 33 34 handleEvent(event) { 35 if (event.type == "load") { 36 const url = event.target.location.href; 37 lazy.logger.debug(`Handle load event with URL ${url}`); 38 this._resolveLoadedURLPromise(url); 39 } 40 } 41 42 actorCreated() { 43 lazy.logger.trace( 44 `[${this.browsingContext.id}] Reftest actor created ` + 45 `for window id ${this.manager.innerWindowId}` 46 ); 47 } 48 49 async receiveMessage(msg) { 50 const { name, data } = msg; 51 52 let result; 53 switch (name) { 54 case "MarionetteReftestParent:flushRendering": 55 result = await this.flushRendering(data); 56 break; 57 case "MarionetteReftestParent:reftestWait": 58 result = await this.reftestWait(data); 59 break; 60 } 61 return result; 62 } 63 64 /** 65 * Wait for a reftest page to be ready for screenshots: 66 * - wait for the loadedURL to be available (see handleEvent) 67 * - check if the URL matches the expected URL 68 * - if present, wait for the "reftest-wait" classname to be removed from the 69 * document element 70 * 71 * @param {object} options 72 * @param {string} options.url 73 * The expected test page URL 74 * @param {boolean} options.useRemote 75 * True when using e10s 76 * @param {boolean} options.warnOnOverflow 77 * True if we should check the content fits in the viewport. 78 * This isn't necessary for print reftests where we will render the full 79 * size of the paginated content. 80 * @returns {boolean} 81 * Returns true when the correct page is loaded and ready for 82 * screenshots. Returns false if the page loaded bug does not have the 83 * expected URL. 84 */ 85 async reftestWait(options = {}) { 86 const { url, useRemote } = options; 87 const loadedURL = await this._loadedURLPromise; 88 if (loadedURL !== url) { 89 lazy.logger.debug( 90 `Window URL does not match the expected URL "${loadedURL}" !== "${url}"` 91 ); 92 return false; 93 } 94 95 const documentElement = this.document.documentElement; 96 const hasReftestWait = documentElement.classList.contains("reftest-wait"); 97 98 lazy.logger.debug("Waiting for event loop to spin"); 99 await new Promise(resolve => lazy.setTimeout(resolve, 0)); 100 101 await this.paintComplete({ 102 useRemote, 103 ignoreThrottledAnimations: true, 104 hasReftestWait, 105 }); 106 107 if (hasReftestWait) { 108 const event = new this.document.defaultView.Event("TestRendered", { 109 bubbles: true, 110 }); 111 documentElement.dispatchEvent(event); 112 lazy.logger.info("Emitted TestRendered event"); 113 await this.reftestWaitRemoved(); 114 await this.paintComplete({ 115 useRemote, 116 ignoreThrottledAnimations: false, 117 hasReftestWait, 118 }); 119 } 120 if ( 121 options.warnOnOverflow && 122 (this.document.defaultView.innerWidth < documentElement.scrollWidth || 123 this.document.defaultView.innerHeight < documentElement.scrollHeight) 124 ) { 125 lazy.logger.warn( 126 `${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})` 127 ); 128 } 129 return true; 130 } 131 132 paintComplete({ useRemote, ignoreThrottledAnimations, hasReftestWait }) { 133 lazy.logger.debug("Waiting for rendering"); 134 let win = this.document.defaultView; 135 let windowUtils = win.windowUtils; 136 let painted = false; 137 const documentElement = this.document.documentElement; 138 return new Promise(resolve => { 139 let maybeResolve = () => { 140 this.flushRendering({ ignoreThrottledAnimations }); 141 if (useRemote) { 142 // Flush display (paint) 143 lazy.logger.debug("Force update of layer tree"); 144 windowUtils.updateLayerTree(); 145 } 146 147 const once = 148 hasReftestWait && !documentElement.classList.contains("reftest-wait"); 149 if (windowUtils.isMozAfterPaintPending && (!once || !painted)) { 150 lazy.logger.debug("isMozAfterPaintPending: true"); 151 win.windowRoot.addEventListener( 152 "MozAfterPaint", 153 () => { 154 lazy.logger.debug("MozAfterPaint fired"); 155 painted = true; 156 maybeResolve(); 157 }, 158 { once: true } 159 ); 160 } else { 161 // resolve at the start of the next frame in case of leftover paints 162 lazy.logger.debug("isMozAfterPaintPending: false"); 163 win.requestAnimationFrame(() => { 164 win.requestAnimationFrame(resolve); 165 }); 166 } 167 }; 168 maybeResolve(); 169 }); 170 } 171 172 reftestWaitRemoved() { 173 lazy.logger.debug("Waiting for reftest-wait removal"); 174 return new Promise(resolve => { 175 const documentElement = this.document.documentElement; 176 let observer = new this.document.defaultView.MutationObserver(() => { 177 if (!documentElement.classList.contains("reftest-wait")) { 178 observer.disconnect(); 179 lazy.logger.debug("reftest-wait removed"); 180 lazy.setTimeout(resolve, 0); 181 } 182 }); 183 if (documentElement.classList.contains("reftest-wait")) { 184 observer.observe(documentElement, { attributes: true }); 185 } else { 186 lazy.setTimeout(resolve, 0); 187 } 188 }); 189 } 190 191 /** 192 * Ensure layout is flushed in each frame 193 * 194 * @param {object} options 195 * @param {boolean} options.ignoreThrottledAnimations Don't flush 196 * the layout of throttled animations. We can end up in a 197 * situation where flushing a throttled animation causes 198 * mozAfterPaint events even when all rendering we care about 199 * should have ceased. See 200 * https://searchfox.org/mozilla-central/rev/d58860eb739af613774c942c3bb61754123e449b/layout/tools/reftest/reftest-content.js#723-729 201 * for more detail. 202 */ 203 flushRendering(options = {}) { 204 let { ignoreThrottledAnimations } = options; 205 lazy.logger.debug( 206 `flushRendering ignoreThrottledAnimations:${ignoreThrottledAnimations}` 207 ); 208 let anyPendingPaintsGeneratedInDescendants = false; 209 210 function flushWindow(win) { 211 let utils = win.windowUtils; 212 let afterPaintWasPending = utils.isMozAfterPaintPending; 213 214 let root = win.document.documentElement; 215 if (root) { 216 try { 217 if (ignoreThrottledAnimations) { 218 utils.flushLayoutWithoutThrottledAnimations(); 219 } else { 220 root.getBoundingClientRect(); 221 } 222 } catch (e) { 223 lazy.logger.error("flushWindow failed", e); 224 } 225 } 226 227 if (!afterPaintWasPending && utils.isMozAfterPaintPending) { 228 anyPendingPaintsGeneratedInDescendants = true; 229 } 230 231 for (let i = 0; i < win.frames.length; ++i) { 232 // Skip remote frames, flushRendering will be called on their individual 233 // MarionetteReftest actor via _recursiveFlushRendering performed from 234 // the topmost MarionetteReftest actor. 235 if (!Cu.isRemoteProxy(win.frames[i])) { 236 flushWindow(win.frames[i]); 237 } 238 } 239 } 240 241 let thisWin = this.document.defaultView; 242 flushWindow(thisWin); 243 244 if ( 245 anyPendingPaintsGeneratedInDescendants && 246 !thisWin.windowUtils.isMozAfterPaintPending 247 ) { 248 lazy.logger.error( 249 "Descendant frame generated a MozAfterPaint event, " + 250 "but the root document doesn't have one!" 251 ); 252 } 253 } 254 }