blob: f6e14128fc07f08984ac705b2f6ec03f02bc2215 [file] [log] [blame]
(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);