test_layout-reflows-observer.js (9044B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test the LayoutChangesObserver 7 8 /* eslint-disable mozilla/use-chromeutils-generateqi */ 9 10 var { 11 getLayoutChangesObserver, 12 releaseLayoutChangesObserver, 13 LayoutChangesObserver, 14 } = require("resource://devtools/server/actors/reflow.js"); 15 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 16 17 // Override set/clearTimeout on LayoutChangesObserver to avoid depending on 18 // time in this unit test. This means that LayoutChangesObserver.eventLoopTimer 19 // will be the timeout callback instead of the timeout itself, so test cases 20 // will need to execute it to fake a timeout 21 LayoutChangesObserver.prototype._setTimeout = cb => cb; 22 LayoutChangesObserver.prototype._clearTimeout = function () {}; 23 24 // Mock the targetActor since we only really want to test the LayoutChangesObserver 25 // and don't want to depend on a window object, nor want to test protocol.js 26 class MockTargetActor extends EventEmitter { 27 constructor() { 28 super(); 29 this.docShell = new MockDocShell(); 30 this.window = new MockWindow(this.docShell); 31 this.windows = [this.window]; 32 this.attached = true; 33 } 34 35 get chromeEventHandler() { 36 return this.docShell.chromeEventHandler; 37 } 38 39 isDestroyed() { 40 return false; 41 } 42 } 43 44 class MockWindow { 45 constructor(docShell) { 46 this.docShell = docShell; 47 } 48 QueryInterface() { 49 const self = this; 50 return { 51 getInterface() { 52 return { 53 QueryInterface() { 54 return self.docShell; 55 }, 56 }; 57 }, 58 }; 59 } 60 setTimeout(cb) { 61 // Simply return the cb itself so that we can execute it in the test instead 62 // of depending on a real timeout 63 return cb; 64 } 65 clearTimeout() {} 66 } 67 68 class MockDocShell { 69 constructor() { 70 this.observer = null; 71 } 72 addWeakReflowObserver(observer) { 73 this.observer = observer; 74 } 75 removeWeakReflowObserver() {} 76 get chromeEventHandler() { 77 return { 78 addEventListener: (type, cb) => { 79 if (type === "resize") { 80 this.resizeCb = cb; 81 } 82 }, 83 removeEventListener: (type, cb) => { 84 if (type === "resize" && cb === this.resizeCb) { 85 this.resizeCb = null; 86 } 87 }, 88 }; 89 } 90 mockResize() { 91 if (this.resizeCb) { 92 this.resizeCb(); 93 } 94 } 95 } 96 97 function run_test() { 98 instancesOfObserversAreSharedBetweenWindows(); 99 eventsAreBatched(); 100 noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts(); 101 observerIsAlreadyStarted(); 102 destroyStopsObserving(); 103 stoppingAndStartingSeveralTimesWorksCorrectly(); 104 reflowsArentStackedWhenStopped(); 105 stackedReflowsAreResetOnStop(); 106 } 107 108 function instancesOfObserversAreSharedBetweenWindows() { 109 info( 110 "Checking that when requesting twice an instances of the observer " + 111 "for the same WindowGlobalTargetActor, the instance is shared" 112 ); 113 114 info("Checking 2 instances of the observer for the targetActor 1"); 115 const targetActor1 = new MockTargetActor(); 116 const obs11 = getLayoutChangesObserver(targetActor1); 117 const obs12 = getLayoutChangesObserver(targetActor1); 118 Assert.equal(obs11, obs12); 119 120 info("Checking 2 instances of the observer for the targetActor 2"); 121 const targetActor2 = new MockTargetActor(); 122 const obs21 = getLayoutChangesObserver(targetActor2); 123 const obs22 = getLayoutChangesObserver(targetActor2); 124 Assert.equal(obs21, obs22); 125 126 info( 127 "Checking that observers instances for 2 different targetActors are " + 128 "different" 129 ); 130 Assert.notEqual(obs11, obs21); 131 132 releaseLayoutChangesObserver(targetActor1); 133 releaseLayoutChangesObserver(targetActor1); 134 releaseLayoutChangesObserver(targetActor2); 135 releaseLayoutChangesObserver(targetActor2); 136 } 137 138 function eventsAreBatched() { 139 info( 140 "Checking that reflow events are batched and only sent when the " + 141 "timeout expires" 142 ); 143 144 // Note that in this test, we mock the target actor and its window property, so we also 145 // mock the setTimeout/clearTimeout mechanism and just call the callback manually 146 const targetActor = new MockTargetActor(); 147 const observer = getLayoutChangesObserver(targetActor); 148 149 const reflowsEvents = []; 150 const onReflows = reflows => reflowsEvents.push(reflows); 151 observer.on("reflows", onReflows); 152 153 const resizeEvents = []; 154 const onResize = () => resizeEvents.push("resize"); 155 observer.on("resize", onResize); 156 157 info("Fake one reflow event"); 158 targetActor.window.docShell.observer.reflow(); 159 info("Checking that no batched reflow event has been emitted"); 160 Assert.equal(reflowsEvents.length, 0); 161 162 info("Fake another reflow event"); 163 targetActor.window.docShell.observer.reflow(); 164 info("Checking that still no batched reflow event has been emitted"); 165 Assert.equal(reflowsEvents.length, 0); 166 167 info("Fake a few of resize events too"); 168 targetActor.window.docShell.mockResize(); 169 targetActor.window.docShell.mockResize(); 170 targetActor.window.docShell.mockResize(); 171 info("Checking that still no batched resize event has been emitted"); 172 Assert.equal(resizeEvents.length, 0); 173 174 info("Faking timeout expiration and checking that events are sent"); 175 observer.eventLoopTimer(); 176 Assert.equal(reflowsEvents.length, 1); 177 Assert.equal(reflowsEvents[0].length, 2); 178 Assert.equal(resizeEvents.length, 1); 179 180 observer.off("reflows", onReflows); 181 observer.off("resize", onResize); 182 releaseLayoutChangesObserver(targetActor); 183 } 184 185 function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() { 186 info( 187 "Checking that if no reflows were detected and the event batching " + 188 "loop expires, then no reflows event is sent" 189 ); 190 191 const targetActor = new MockTargetActor(); 192 const observer = getLayoutChangesObserver(targetActor); 193 194 const reflowsEvents = []; 195 const onReflows = reflows => reflowsEvents.push(reflows); 196 observer.on("reflows", onReflows); 197 198 info("Faking timeout expiration and checking for reflows"); 199 observer.eventLoopTimer(); 200 Assert.equal(reflowsEvents.length, 0); 201 202 observer.off("reflows", onReflows); 203 releaseLayoutChangesObserver(targetActor); 204 } 205 206 function observerIsAlreadyStarted() { 207 info("Checking that the observer is already started when getting it"); 208 209 const targetActor = new MockTargetActor(); 210 const observer = getLayoutChangesObserver(targetActor); 211 Assert.ok(observer.isObserving); 212 213 observer.stop(); 214 Assert.ok(!observer.isObserving); 215 216 observer.start(); 217 Assert.ok(observer.isObserving); 218 219 releaseLayoutChangesObserver(targetActor); 220 } 221 222 function destroyStopsObserving() { 223 info("Checking that the destroying the observer stops it"); 224 225 const targetActor = new MockTargetActor(); 226 const observer = getLayoutChangesObserver(targetActor); 227 Assert.ok(observer.isObserving); 228 229 observer.destroy(); 230 Assert.ok(!observer.isObserving); 231 232 releaseLayoutChangesObserver(targetActor); 233 } 234 235 function stoppingAndStartingSeveralTimesWorksCorrectly() { 236 info( 237 "Checking that the stopping and starting several times the observer" + 238 " works correctly" 239 ); 240 241 const targetActor = new MockTargetActor(); 242 const observer = getLayoutChangesObserver(targetActor); 243 244 Assert.ok(observer.isObserving); 245 observer.start(); 246 observer.start(); 247 observer.start(); 248 Assert.ok(observer.isObserving); 249 250 observer.stop(); 251 Assert.ok(!observer.isObserving); 252 253 observer.stop(); 254 observer.stop(); 255 Assert.ok(!observer.isObserving); 256 257 releaseLayoutChangesObserver(targetActor); 258 } 259 260 function reflowsArentStackedWhenStopped() { 261 info("Checking that when stopped, reflows aren't stacked in the observer"); 262 263 const targetActor = new MockTargetActor(); 264 const observer = getLayoutChangesObserver(targetActor); 265 266 info("Stoping the observer"); 267 observer.stop(); 268 269 info("Faking reflows"); 270 targetActor.window.docShell.observer.reflow(); 271 targetActor.window.docShell.observer.reflow(); 272 targetActor.window.docShell.observer.reflow(); 273 274 info("Checking that reflows aren't recorded"); 275 Assert.equal(observer.reflows.length, 0); 276 277 info("Starting the observer and faking more reflows"); 278 observer.start(); 279 targetActor.window.docShell.observer.reflow(); 280 targetActor.window.docShell.observer.reflow(); 281 targetActor.window.docShell.observer.reflow(); 282 283 info("Checking that reflows are recorded"); 284 Assert.equal(observer.reflows.length, 3); 285 286 releaseLayoutChangesObserver(targetActor); 287 } 288 289 function stackedReflowsAreResetOnStop() { 290 info("Checking that stacked reflows are reset on stop"); 291 292 const targetActor = new MockTargetActor(); 293 const observer = getLayoutChangesObserver(targetActor); 294 295 targetActor.window.docShell.observer.reflow(); 296 Assert.equal(observer.reflows.length, 1); 297 298 observer.stop(); 299 Assert.equal(observer.reflows.length, 0); 300 301 targetActor.window.docShell.observer.reflow(); 302 Assert.equal(observer.reflows.length, 0); 303 304 observer.start(); 305 Assert.equal(observer.reflows.length, 0); 306 307 targetActor.window.docShell.observer.reflow(); 308 Assert.equal(observer.reflows.length, 1); 309 310 releaseLayoutChangesObserver(targetActor); 311 }