| (function(root) { |
| 'use strict'; |
| // |
| var index = 0; |
| var suite = root.generalParallelTest = { |
| // prepare individual test |
| setup: function(data, options) { |
| suite._setupDom(data, options); |
| suite._setupEvents(data, options); |
| }, |
| // clone fixture and prepare data containers |
| _setupDom: function(data, options) { |
| // clone fixture into off-viewport test-canvas |
| data.fixture = document.getElementById('fixture').cloneNode(true); |
| data.fixture.id = 'test-' + (index++); |
| (document.getElementById('offscreen') || document.body).appendChild(data.fixture); |
| |
| // data container for #fixture > .container > .transition |
| data.transition = { |
| node: data.fixture.querySelector('.transition'), |
| values: [], |
| events: [], |
| computedStyle: function(property) { |
| return computedStyle(data.transition.node, property); |
| } |
| }; |
| |
| // data container for #fixture > .container |
| data.container = { |
| node: data.transition.node.parentNode, |
| values: [], |
| events: [], |
| computedStyle: function(property) { |
| return computedStyle(data.container.node, property); |
| } |
| }; |
| |
| // data container for #fixture > .container > .transition[:before | :after] |
| if (data.pseudo) { |
| data.pseudo = { |
| name: data.pseudo, |
| values: [], |
| computedStyle: function(property) { |
| return computedStyle(data.transition.node, property, ':' + data.pseudo.name); |
| } |
| }; |
| } |
| }, |
| // bind TransitionEnd event listeners |
| _setupEvents: function(data, options) { |
| ['transition', 'container'].forEach(function(elem) { |
| var handler = function(event) { |
| event.stopPropagation(); |
| var name = event.propertyName; |
| var time = Math.round(event.elapsedTime * 1000) / 1000; |
| var pseudo = event.pseudoElement ? (':' + event.pseudoElement) : ''; |
| data[elem].events.push(name + pseudo + ":" + time + "s"); |
| }; |
| data[elem].node.addEventListener('transitionend', handler, false); |
| data[elem]._events = {'transitionend': handler}; |
| }); |
| }, |
| // cleanup after individual test |
| teardown: function(data, options) { |
| // data.fixture.remove(); |
| if (data.fixture.parentNode) { |
| data.fixture.parentNode.removeChild(data.fixture); |
| } |
| }, |
| // invoked prior to running a slice of tests |
| sliceStart: function(options, tests) { |
| // inject styles into document |
| setStyle(options.styles); |
| // kick off value collection loop |
| generalParallelTest.startValueCollection(options); |
| }, |
| // invoked after running a slice of tests |
| sliceDone: function(options, tests) { |
| // stop value collection loop |
| generalParallelTest.stopValueCollection(options); |
| // reset styles cache |
| options.styles = {}; |
| }, |
| // called once all tests are done |
| done: function(options) { |
| // reset document styles |
| setStyle(); |
| reflow(); |
| }, |
| // add styles of individual test to slice cache |
| addStyles: function(data, options, styles) { |
| if (!options.styles) { |
| options.styles = {}; |
| } |
| |
| Object.keys(styles).forEach(function(key) { |
| var selector = '#' + data.fixture.id |
| // fixture must become #fixture.fixture rather than a child selector |
| + (key.substring(0, 8) === '.fixture' ? '' : ' ') |
| + key; |
| |
| options.styles[selector] = styles[key]; |
| }); |
| }, |
| // set style and compute values for container and transition |
| getStyle: function(data) { |
| reflow(); |
| // grab current styles: "initial state" |
| suite._getStyleFor(data, 'from'); |
| // apply target state |
| suite._addClass(data, 'to', true); |
| // grab current styles: "target state" |
| suite._getStyleFor(data, 'to'); |
| // remove target state |
| suite._removeClass(data, 'to', true); |
| |
| // clean up the mess created for value collection |
| data.container._values = []; |
| data.transition._values = []; |
| if (data.pseudo) { |
| data.pseudo._values = []; |
| } |
| }, |
| // grab current styles and store in respective element's data container |
| _getStyleFor: function(data, key) { |
| data.container[key] = data.container.computedStyle(data.property); |
| data.transition[key] = data.transition.computedStyle(data.property); |
| if (data.pseudo) { |
| data.pseudo[key] = data.pseudo.computedStyle(data.property); |
| } |
| }, |
| // add class to test's elements and possibly reflow |
| _addClass: function(data, className, forceReflow) { |
| data.container.node.classList.add(className); |
| data.transition.node.classList.add(className); |
| if (forceReflow) { |
| reflow(); |
| } |
| }, |
| // remove class from test's elements and possibly reflow |
| _removeClass: function(data, className, forceReflow) { |
| data.container.node.classList.remove(className); |
| data.transition.node.classList.remove(className); |
| if (forceReflow) { |
| reflow(); |
| } |
| }, |
| // add transition and to classes to container and transition |
| startTransition: function(data) { |
| // add transition-defining class |
| suite._addClass(data, 'how', true); |
| // add target state (without reflowing) |
| suite._addClass(data, 'to', false); |
| }, |
| // requestAnimationFrame runLoop to collect computed values |
| startValueCollection: function(options) { |
| var raf = window.requestAnimationFrame || function(callback){ |
| setTimeout(callback, 20); |
| }; |
| |
| // flag denoting if the runLoop should continue (true) or exit (false) |
| options._collectValues = true; |
| |
| function runLoop() { |
| if (!options._collectValues) { |
| // test's are done, stop annoying the CPU |
| return; |
| } |
| |
| // collect current style for test's elements |
| options.tests.forEach(function(data) { |
| if (!data.property) { |
| return; |
| } |
| |
| ['transition', 'container', 'pseudo'].forEach(function(elem) { |
| var pseudo = null; |
| if (!data[elem] || (elem === 'pseudo' && !data.pseudo)) { |
| return; |
| } |
| |
| var current = data[elem].computedStyle(data.property); |
| var values = data[elem].values; |
| var length = values.length; |
| if (!length || values[length - 1] !== current) { |
| values.push(current); |
| } |
| }); |
| }); |
| |
| // rinse and repeat |
| raf(runLoop); |
| } |
| |
| runLoop(); |
| }, |
| // stop requestAnimationFrame runLoop collecting computed values |
| stopValueCollection: function(options) { |
| options._collectValues = false; |
| }, |
| |
| // generate test.step function asserting collected events match expected |
| assertExpectedEventsFunc: function(data, elem, expected) { |
| return function() { |
| var _result = data[elem].events.sort().join(" "); |
| var _expected = typeof expected === 'string' ? expected : expected.sort().join(" "); |
| assert_equals(_result, _expected, "Expected TransitionEnd events triggered on ." + elem); |
| }; |
| }, |
| // generate test.step function asserting collected values are neither initial nor target |
| assertIntermediateValuesFunc: function(data, elem) { |
| return function() { |
| // the first value (index: 0) is always going to be the initial value |
| // the last value is always going to be the target value |
| var values = data[elem].values; |
| if (data.flags.discrete) { |
| // a discrete value will just switch from one state to another without having passed intermediate states. |
| assert_equals(values[0], data[elem].from, "must be initial value while transitioning on ." + elem); |
| assert_equals(values[1], data[elem].to, "must be target value after transitioning on ." + elem); |
| assert_equals(values.length, 2, "discrete property only has 2 values ." + elem); |
| } else { |
| assert_not_equals(values[1], data[elem].from, "may not be initial value while transitioning on ." + elem); |
| assert_not_equals(values[1], data[elem].to, "may not be target value while transitioning on ." + elem); |
| } |
| |
| // TODO: first value must be initial, last value must be target |
| }; |
| } |
| }; |
| |
| })(window); |