resizeTestHelper.js (5698B)
1 'use strict'; 2 3 /** 4 ResizeTestHelper is a framework to test ResizeObserver 5 notifications. Use it to make assertions about ResizeObserverEntries. 6 This framework is needed because ResizeObservations are 7 delivered asynchronously inside the event loop. 8 9 Features: 10 - can queue multiple notification steps in a test 11 - handles timeouts 12 - returns Promise that is fulfilled when test completes. 13 Use to chain tests (since parallel async ResizeObserver tests 14 would conflict if reusing same DOM elements). 15 16 Usage: 17 18 create ResizeTestHelper for every test. 19 Make assertions inside notify, timeout callbacks. 20 Start tests with helper.start() 21 Chain tests with Promises. 22 Counts animation frames, see startCountingRaf 23 */ 24 25 /* 26 @param name: test name 27 @param steps: 28 { 29 setup: function(ResizeObserver) { 30 // called at the beginning of the test step 31 // your observe/resize code goes here 32 }, 33 notify: function(entries, observer) { 34 // ResizeObserver callback. 35 // Make assertions here. 36 // Return true if next step should start on the next event loop. 37 }, 38 timeout: function() { 39 // Define this if your test expects to time out, and the expected timeout 40 // value will be 100ms. 41 // If undefined, timeout is assert_unreached after 1000ms. 42 } 43 } 44 */ 45 function ResizeTestHelper(name, steps) 46 { 47 this._name = name; 48 this._steps = steps || []; 49 this._stepIdx = -1; 50 this._harnessTest = null; 51 this._observer = new ResizeObserver(this._handleNotification.bind(this)); 52 this._timeoutBind = this._handleTimeout.bind(this); 53 this._nextStepBind = this._nextStep.bind(this); 54 } 55 56 // The default timeout value in ms. 57 // We expect TIMEOUT to be longer than we would ever have to wait for notify() 58 // to be fired. This is used for tests which are not expected to time out, so 59 // it can be large without slowing down the test. 60 ResizeTestHelper.TIMEOUT = 1000; 61 // A reasonable short timeout value in ms. 62 // We expect SHORT_TIMEOUT to be long enough that notify() would usually get a 63 // chance to fire before SHORT_TIMEOUT expires. This is used for tests which 64 // *are* expected to time out (with no callbacks fired), so we'd like to keep 65 // it relatively short to avoid slowing down the test. 66 ResizeTestHelper.SHORT_TIMEOUT = 100; 67 68 ResizeTestHelper.prototype = { 69 get _currentStep() { 70 return this._steps[this._stepIdx]; 71 }, 72 73 _nextStep: function() { 74 if (++this._stepIdx == this._steps.length) 75 return this._done(); 76 // Use SHORT_TIMEOUT if this step expects timeout. 77 let timeoutValue = this._steps[this._stepIdx].timeout ? 78 ResizeTestHelper.SHORT_TIMEOUT : 79 ResizeTestHelper.TIMEOUT; 80 this._timeoutId = this._harnessTest.step_timeout( 81 this._timeoutBind, timeoutValue); 82 try { 83 this._steps[this._stepIdx].setup(this._observer); 84 } 85 catch(err) { 86 this._harnessTest.step(() => { 87 assert_unreached("Caught a throw, possible syntax error"); 88 }); 89 } 90 }, 91 92 _handleNotification: function(entries) { 93 if (this._timeoutId) { 94 window.clearTimeout(this._timeoutId); 95 delete this._timeoutId; 96 } 97 this._harnessTest.step(() => { 98 try { 99 let rafDelay = this._currentStep.notify(entries, this._observer); 100 if (rafDelay) 101 window.requestAnimationFrame(this._nextStepBind); 102 else 103 this._nextStep(); 104 } 105 catch(err) { 106 this._harnessTest.step(() => { 107 throw err; 108 }); 109 // Force to _done() the current test. 110 this._done(); 111 } 112 }); 113 }, 114 115 _handleTimeout: function() { 116 delete this._timeoutId; 117 this._harnessTest.step(() => { 118 if (this._currentStep.timeout) { 119 this._currentStep.timeout(); 120 } 121 else { 122 this._harnessTest.step(() => { 123 assert_unreached("Timed out waiting for notification. (" + ResizeTestHelper.TIMEOUT + "ms)"); 124 }); 125 } 126 this._nextStep(); 127 }); 128 }, 129 130 _done: function() { 131 this._observer.disconnect(); 132 delete this._observer; 133 this._harnessTest.done(); 134 if (this._rafCountRequest) { 135 window.cancelAnimationFrame(this._rafCountRequest); 136 delete this._rafCountRequest; 137 } 138 window.requestAnimationFrame(() => { this._resolvePromise(); }); 139 }, 140 141 start: function(cleanup) { 142 this._harnessTest = async_test(this._name); 143 144 if (cleanup) { 145 this._harnessTest.add_cleanup(cleanup); 146 } 147 148 this._harnessTest.step(() => { 149 assert_equals(this._stepIdx, -1, "start can only be called once"); 150 this._nextStep(); 151 }); 152 return new Promise( (resolve, reject) => { 153 this._resolvePromise = resolve; 154 this._rejectPromise = reject; 155 }); 156 }, 157 158 get rafCount() { 159 if (!this._rafCountRequest) 160 throw "rAF count is not active"; 161 return this._rafCount; 162 }, 163 164 get test() { 165 if (!this._harnessTest) { 166 throw "_harnessTest is not initialized"; 167 } 168 return this._harnessTest; 169 }, 170 171 _incrementRaf: function() { 172 if (this._rafCountRequest) { 173 this._rafCount++; 174 this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind); 175 } 176 }, 177 178 startCountingRaf: function() { 179 if (this._rafCountRequest) 180 window.cancelAnimationFrame(this._rafCountRequest); 181 if (!this._incrementRafBind) 182 this._incrementRafBind = this._incrementRaf.bind(this); 183 this._rafCount = 0; 184 this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind); 185 } 186 } 187 188 function createAndAppendElement(tagName, parent) { 189 if (!parent) { 190 parent = document.body; 191 } 192 const element = document.createElement(tagName); 193 parent.appendChild(element); 194 return element; 195 }