generalParallelTest.js (9937B)
1 (function(root) { 2 'use strict'; 3 4 var index = 0; 5 var suite = root.generalParallelTest = { 6 // prepare individual test 7 setup: function(data, options) { 8 suite._setupDom(data, options); 9 suite._setupEvents(data, options); 10 }, 11 // clone fixture and prepare data containers 12 _setupDom: function(data, options) { 13 // clone fixture into off-viewport test-canvas 14 data.fixture = document.getElementById('fixture').cloneNode(true); 15 data.fixture.id = 'test-' + (index++); 16 (document.getElementById('offscreen') || document.body).appendChild(data.fixture); 17 18 // data container for #fixture > .container > .transition 19 data.transition = { 20 node: data.fixture.querySelector('.transition'), 21 values: [], 22 events: [], 23 computedStyle: function(property) { 24 return computedStyle(data.transition.node, property); 25 } 26 }; 27 28 // data container for #fixture > .container 29 data.container = { 30 node: data.transition.node.parentNode, 31 values: [], 32 events: [], 33 computedStyle: function(property) { 34 return computedStyle(data.container.node, property); 35 } 36 }; 37 38 // data container for #fixture > .container > .transition[:before | :after] 39 if (data.pseudo) { 40 data.pseudo = { 41 name: data.pseudo, 42 values: [], 43 computedStyle: function(property) { 44 return computedStyle(data.transition.node, property, ':' + data.pseudo.name); 45 } 46 }; 47 } 48 }, 49 // bind TransitionEnd event listeners 50 _setupEvents: function(data, options) { 51 ['transition', 'container'].forEach(function(elem) { 52 var handler = function(event) { 53 event.stopPropagation(); 54 var name = event.propertyName; 55 var time = Math.round(event.elapsedTime * 1000) / 1000; 56 var pseudo = event.pseudoElement ? (':' + event.pseudoElement) : ''; 57 data[elem].events.push(name + pseudo + ":" + time + "s"); 58 }; 59 data[elem].node.addEventListener('transitionend', handler, false); 60 data[elem]._events = {'transitionend': handler}; 61 }); 62 }, 63 // cleanup after individual test 64 teardown: function(data, options) { 65 // data.fixture.remove(); 66 if (data.fixture.parentNode) { 67 data.fixture.parentNode.removeChild(data.fixture); 68 } 69 }, 70 // invoked prior to running a slice of tests 71 sliceStart: function(options, tests) { 72 // inject styles into document 73 setStyle(options.styles); 74 }, 75 transitionsStarted: function(options, tests) { 76 // kick off value collection loop 77 generalParallelTest.startValueCollection(options); 78 }, 79 // invoked after running a slice of tests 80 sliceDone: function(options, tests) { 81 // stop value collection loop 82 generalParallelTest.stopValueCollection(options); 83 // reset styles cache 84 options.styles = {}; 85 }, 86 // called once all tests are done 87 done: function(options) { 88 // reset document styles 89 setStyle(); 90 reflow(); 91 }, 92 // add styles of individual test to slice cache 93 addStyles: function(data, options, styles) { 94 if (!options.styles) { 95 options.styles = {}; 96 } 97 98 Object.keys(styles).forEach(function(key) { 99 var selector = '#' + data.fixture.id 100 // fixture must become #fixture.fixture rather than a child selector 101 + (key.substring(0, 8) === '.fixture' ? '' : ' ') 102 + key; 103 104 options.styles[selector] = styles[key]; 105 }); 106 }, 107 // set style and compute values for container and transition 108 getStyle: function(data) { 109 reflow(); 110 // grab current styles: "initial state" 111 suite._getStyleFor(data, 'from'); 112 // apply target state 113 suite._addClass(data, 'to', true); 114 // grab current styles: "target state" 115 suite._getStyleFor(data, 'to'); 116 // remove target state 117 suite._removeClass(data, 'to', true); 118 119 // clean up the mess created for value collection 120 data.container._values = []; 121 data.transition._values = []; 122 if (data.pseudo) { 123 data.pseudo._values = []; 124 } 125 }, 126 // grab current styles and store in respective element's data container 127 _getStyleFor: function(data, key) { 128 data.container[key] = data.container.computedStyle(data.property); 129 data.transition[key] = data.transition.computedStyle(data.property); 130 if (data.pseudo) { 131 data.pseudo[key] = data.pseudo.computedStyle(data.property); 132 } 133 }, 134 // add class to test's elements and possibly reflow 135 _addClass: function(data, className, forceReflow) { 136 data.container.node.classList.add(className); 137 data.transition.node.classList.add(className); 138 if (forceReflow) { 139 reflow(); 140 } 141 }, 142 // remove class from test's elements and possibly reflow 143 _removeClass: function(data, className, forceReflow) { 144 data.container.node.classList.remove(className); 145 data.transition.node.classList.remove(className); 146 if (forceReflow) { 147 reflow(); 148 } 149 }, 150 // add transition and to classes to container and transition 151 startTransition: function(data) { 152 // add transition-defining class 153 suite._addClass(data, 'how', true); 154 // add target state (without reflowing) 155 suite._addClass(data, 'to', false); 156 }, 157 // requestAnimationFrame runLoop to collect computed values 158 startValueCollection: function(options) { 159 const promises = []; 160 const animations = document.getAnimations(); 161 animations.forEach(a => { 162 promises.push(new Promise(resolve => { 163 const listener = (event) => { 164 let found = false; 165 if (event.pseudoElement != '') { 166 if (event.pseudoElement == a.effect.pseudoElement) { 167 found = true; 168 } 169 } else if (!a.effect.pseudoElement) { 170 found = true; 171 } 172 if (found) { 173 a.effect.target.removeEventListener( 174 'transitionend', listener); 175 resolve(); 176 } 177 }; 178 a.effect.target.addEventListener('transitionend', listener); 179 })); 180 }); 181 Promise.all(promises).then(() => { 182 options.allTransitionsCompleted(); 183 }); 184 185 // Sample at these values expressed as relative progress. 186 // The last sample being the end of the transition ensure that the 187 // completion events are received in short order. 188 const sample_at = [0.2, 0.4, 0.6, 0.8, 1.0]; 189 sample_at.forEach(at => { 190 const time = options.duration * at; 191 animations.forEach(a => { a.currentTime = time; }); 192 // Collect current style for test's elements. 193 options.tests.forEach(function(data) { 194 if (!data.property) { 195 return; 196 } 197 198 ['transition', 'container', 'pseudo'].forEach(function(elem) { 199 var pseudo = null; 200 if (!data[elem] || (elem === 'pseudo' && !data.pseudo)) { 201 return; 202 } 203 204 var current = data[elem].computedStyle(data.property); 205 var values = data[elem].values; 206 var length = values.length; 207 if (!length || values[length - 1] !== current) { 208 values.push(current); 209 } 210 }); 211 }); 212 }); 213 }, 214 // stop requestAnimationFrame runLoop collecting computed values 215 stopValueCollection: function(options) { 216 options._collectValues = false; 217 }, 218 219 // generate test.step function asserting collected events match expected 220 assertExpectedEventsFunc: function(data, elem, expected) { 221 return function() { 222 var _result = data[elem].events.sort().join(" "); 223 var _expected = typeof expected === 'string' ? expected : expected.sort().join(" "); 224 assert_equals(_result, _expected, "Expected TransitionEnd events triggered on ." + elem); 225 }; 226 }, 227 // generate test.step function asserting collected values are neither initial nor target 228 assertIntermediateValuesFunc: function(data, elem) { 229 return function() { 230 // the first value (index: 0) is always going to be the initial value 231 // the last value is always going to be the target value 232 var values = data[elem].values; 233 if (data.flags.discrete) { 234 // a discrete value will just switch from one state to another without having passed intermediate states. 235 assert_equals(values[0], data[elem].from, "must be initial value while transitioning on ." + elem); 236 assert_equals(values[1], data[elem].to, "must be target value after transitioning on ." + elem); 237 assert_equals(values.length, 2, "discrete property only has 2 values ." + elem); 238 } else { 239 assert_not_equals(values[1], data[elem].from, "may not be initial value while transitioning on ." + elem); 240 assert_not_equals(values[1], data[elem].to, "may not be target value while transitioning on ." + elem); 241 } 242 243 // TODO: first value must be initial, last value must be target 244 }; 245 } 246 }; 247 248 })(window);