helper.js (10235B)
1 // 2 // Simple Helper Functions For Testing CSS 3 // 4 5 (function(root) { 6 'use strict'; 7 8 // serialize styles object and dump to dom 9 // appends <style id="dynamic-style"> to <head> 10 // setStyle("#some-selector", {"some-style" : "value"}) 11 // setStyle({"#some-selector": {"some-style" : "value"}}) 12 root.setStyle = function(selector, styles) { 13 var target = document.getElementById('dynamic-style'); 14 if (!target) { 15 target = document.createElement('style'); 16 target.id = 'dynamic-style'; 17 target.type = "text/css"; 18 document.getElementsByTagName('head')[0].appendChild(target); 19 } 20 21 var data = []; 22 // single selector/styles 23 if (typeof selector === 'string' && styles !== undefined) { 24 data = [selector, '{', serializeStyles(styles), '}']; 25 target.textContent = data.join("\n"); 26 return; 27 } 28 // map of selector/styles 29 for (var key in selector) { 30 if (Object.prototype.hasOwnProperty.call(selector, key)) { 31 var _data = [key, '{', serializeStyles(selector[key]), '}']; 32 data.push(_data.join('\n')); 33 } 34 } 35 36 target.textContent = data.join("\n"); 37 }; 38 39 function serializeStyles(styles) { 40 var data = []; 41 for (var property in styles) { 42 if (Object.prototype.hasOwnProperty.call(styles, property)) { 43 var prefixedProperty = addVendorPrefix(property); 44 data.push(prefixedProperty + ":" + styles[property] + ";"); 45 } 46 } 47 48 return data.join('\n'); 49 } 50 51 52 // shorthand for computed style 53 root.computedStyle = function(element, property, pseudo) { 54 var prefixedProperty = addVendorPrefix(property); 55 return window 56 .getComputedStyle(element, pseudo || null) 57 .getPropertyValue(prefixedProperty); 58 }; 59 60 // flush rendering buffer 61 root.reflow = function() { 62 document.body.offsetWidth; 63 }; 64 65 // merge objects 66 root.extend = function(target /*, ..rest */) { 67 Array.prototype.slice.call(arguments, 1).forEach(function(obj) { 68 Object.keys(obj).forEach(function(key) { 69 target[key] = obj[key]; 70 }); 71 }); 72 73 return target; 74 }; 75 76 // dom fixture helper ("resetting dom test elements") 77 var _domFixture; 78 var _domFixtureSelector; 79 root.domFixture = function(selector) { 80 var fixture = document.querySelector(selector || _domFixtureSelector); 81 if (!fixture) { 82 throw new Error('fixture ' + (selector || _domFixtureSelector) + ' not found!'); 83 } 84 if (!_domFixture && selector) { 85 // save a copy 86 _domFixture = fixture.cloneNode(true); 87 _domFixtureSelector = selector; 88 } else if (_domFixture) { 89 // restore the copy 90 var tmp = _domFixture.cloneNode(true); 91 fixture.parentNode.replaceChild(tmp, fixture); 92 } else { 93 throw new Error('domFixture must be initialized first!'); 94 } 95 }; 96 97 root.MS_PER_SEC = 1000; 98 99 /* 100 * The recommended minimum precision to use for time values. 101 * 102 * Based on Web Animations: 103 * https://w3c.github.io/web-animations/#precision-of-time-values 104 */ 105 const TIME_PRECISION = 0.0005; // ms 106 107 /* 108 * Allow implementations to substitute an alternative method for comparing 109 * times based on their precision requirements. 110 */ 111 root.assert_times_equal = function(actual, expected, description) { 112 assert_approx_equals(actual, expected, TIME_PRECISION, description); 113 }; 114 115 /* 116 * Compare a time value based on its precision requirements with a fixed value. 117 */ 118 root.assert_time_equals_literal = (actual, expected, description) => { 119 assert_approx_equals(actual, expected, TIME_PRECISION, description); 120 }; 121 122 /** 123 * Assert that CSSTransition event, |evt|, has the expected property values 124 * defined by |propertyName|, |elapsedTime|, and |pseudoElement|. 125 */ 126 root.assert_end_events_equal = function(evt, propertyName, elapsedTime, 127 pseudoElement = '') { 128 assert_equals(evt.propertyName, propertyName); 129 assert_times_equal(evt.elapsedTime, elapsedTime); 130 assert_equals(evt.pseudoElement, pseudoElement); 131 }; 132 133 /** 134 * Assert that array of simultaneous CSSTransition events, |evts|, have the 135 * corresponding property names listed in |propertyNames|, and the expected 136 * |elapsedTimes| and |pseudoElement| members. 137 * 138 * |elapsedTimes| may be a single value if all events are expected to have the 139 * same elapsedTime, or an array parallel to |propertyNames|. 140 */ 141 root.assert_end_event_batch_equal = function(evts, propertyNames, elapsedTimes, 142 pseudoElement = '') { 143 assert_equals( 144 evts.length, 145 propertyNames.length, 146 'Test harness error: should have waited for the correct number of events' 147 ); 148 assert_true( 149 typeof elapsedTimes === 'number' || 150 (Array.isArray(elapsedTimes) && 151 elapsedTimes.length === propertyNames.length), 152 'Test harness error: elapsedTimes must either be a number or an array of' + 153 ' numbers with the same length as propertyNames' 154 ); 155 156 if (typeof elapsedTimes === 'number') { 157 elapsedTimes = Array(propertyNames.length).fill(elapsedTimes); 158 } 159 const testPairs = propertyNames.map((propertyName, index) => ({ 160 propertyName, 161 elapsedTime: elapsedTimes[index] 162 })); 163 164 const sortByPropertyName = (a, b) => 165 a.propertyName.localeCompare(b.propertyName); 166 evts.sort(sortByPropertyName); 167 testPairs.sort(sortByPropertyName); 168 169 for (let evt of evts) { 170 const expected = testPairs.shift(); 171 assert_end_events_equal( 172 evt, 173 expected.propertyName, 174 expected.elapsedTime, 175 pseudoElement 176 ); 177 } 178 } 179 180 /** 181 * Appends a div to the document body. 182 * 183 * @param t The testharness.js Test object. If provided, this will be used 184 * to register a cleanup callback to remove the div when the test 185 * finishes. 186 * 187 * @param attrs A dictionary object with attribute names and values to set on 188 * the div. 189 */ 190 root.addDiv = function(t, attrs) { 191 var div = document.createElement('div'); 192 if (attrs) { 193 for (var attrName in attrs) { 194 div.setAttribute(attrName, attrs[attrName]); 195 } 196 } 197 document.body.appendChild(div); 198 if (t && typeof t.add_cleanup === 'function') { 199 t.add_cleanup(function() { 200 if (div.parentNode) { 201 div.remove(); 202 } 203 }); 204 } 205 return div; 206 }; 207 208 /** 209 * Appends a style div to the document head. 210 * 211 * @param t The testharness.js Test object. If provided, this will be used 212 * to register a cleanup callback to remove the style element 213 * when the test finishes. 214 * 215 * @param rules A dictionary object with selector names and rules to set on 216 * the style sheet. 217 */ 218 root.addStyle = (t, rules) => { 219 const extraStyle = document.createElement('style'); 220 document.head.appendChild(extraStyle); 221 if (rules) { 222 const sheet = extraStyle.sheet; 223 for (const selector in rules) { 224 sheet.insertRule(selector + '{' + rules[selector] + '}', 225 sheet.cssRules.length); 226 } 227 } 228 229 if (t && typeof t.add_cleanup === 'function') { 230 t.add_cleanup(() => { 231 extraStyle.remove(); 232 }); 233 } 234 return extraStyle; 235 }; 236 237 /** 238 * Promise wrapper for requestAnimationFrame. 239 */ 240 root.waitForFrame = () => { 241 return new Promise(resolve => { 242 window.requestAnimationFrame(resolve); 243 }); 244 }; 245 246 /** 247 * Returns a Promise that is resolved after the given number of consecutive 248 * animation frames have occured (using requestAnimationFrame callbacks). 249 * 250 * @param frameCount The number of animation frames. 251 * @param onFrame An optional function to be processed in each animation frame. 252 */ 253 root.waitForAnimationFrames = (frameCount, onFrame) => { 254 const timeAtStart = document.timeline.currentTime; 255 return new Promise(resolve => { 256 function handleFrame() { 257 if (onFrame && typeof onFrame === 'function') { 258 onFrame(); 259 } 260 if (timeAtStart != document.timeline.currentTime && 261 --frameCount <= 0) { 262 resolve(); 263 } else { 264 window.requestAnimationFrame(handleFrame); // wait another frame 265 } 266 } 267 window.requestAnimationFrame(handleFrame); 268 }); 269 }; 270 271 /** 272 * Wrapper that takes a sequence of N animations and returns: 273 * 274 * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]); 275 */ 276 root.waitForAllAnimations = animations => 277 Promise.all(animations.map(animation => animation.ready)); 278 279 /** 280 * Utility that takes a Promise and a maximum number of frames to wait and 281 * returns a new Promise that behaves as follows: 282 * 283 * - If the provided Promise resolves _before_ the specified number of frames 284 * have passed, resolves with the result of the provided Promise. 285 * - If the provided Promise rejects _before_ the specified number of frames 286 * have passed, rejects with the error result of the provided Promise. 287 * - Otherwise, rejects with a 'Timed out' error message. If |message| is 288 * provided, it will be appended to the error message. 289 */ 290 root.frameTimeout = (promiseToWaitOn, framesToWait, message) => { 291 let framesRemaining = framesToWait; 292 let aborted = false; 293 294 const timeoutPromise = new Promise(function waitAFrame(resolve, reject) { 295 if (aborted) { 296 resolve(); 297 return; 298 } 299 if (framesRemaining-- > 0) { 300 requestAnimationFrame(() => { 301 waitAFrame(resolve, reject); 302 }); 303 return; 304 } 305 let errorMessage = 'Timed out waiting for Promise to resolve'; 306 if (message) { 307 errorMessage += `: ${message}`; 308 } 309 reject(new Error(errorMessage)); 310 }); 311 312 const wrappedPromiseToWaitOn = promiseToWaitOn.then(result => { 313 aborted = true; 314 return result; 315 }); 316 317 return Promise.race([timeoutPromise, wrappedPromiseToWaitOn]); 318 }; 319 320 root.supportsStartingStyle = () => { 321 let sheet = new CSSStyleSheet(); 322 sheet.replaceSync("@starting-style{}"); 323 return sheet.cssRules.length == 1; 324 }; 325 326 /** 327 * Waits for a 'transitionend' event to fire on the given element. 328 * 329 * @param element The DOM element to listen for the transitionend event on. 330 * @returns {Promise<void>} A promise that resolves when the transitionend event is fired. 331 */ 332 root.waitForTransitionEnd = function(element) { 333 return new Promise(resolve => { 334 element.addEventListener('transitionend', resolve, { once: true }); 335 }); 336 }; 337 338 339 })(window);