blob: d5fa61b15eff12f48d0a76df51595b104579f757 [file] [log] [blame]
//
// Simple Helper Functions For Testing CSS
//
(function(root) {
'use strict';
// serialize styles object and dump to dom
// appends <style id="dynamic-style"> to <head>
// setStyle("#some-selector", {"some-style" : "value"})
// setStyle({"#some-selector": {"some-style" : "value"}})
root.setStyle = function(selector, styles) {
var target = document.getElementById('dynamic-style');
if (!target) {
target = document.createElement('style');
target.id = 'dynamic-style';
target.type = "text/css";
document.getElementsByTagName('head')[0].appendChild(target);
}
var data = [];
// single selector/styles
if (typeof selector === 'string' && styles !== undefined) {
data = [selector, '{', serializeStyles(styles), '}'];
target.textContent = data.join("\n");
return;
}
// map of selector/styles
for (var key in selector) {
if (Object.prototype.hasOwnProperty.call(selector, key)) {
var _data = [key, '{', serializeStyles(selector[key]), '}'];
data.push(_data.join('\n'));
}
}
target.textContent = data.join("\n");
};
function serializeStyles(styles) {
var data = [];
for (var property in styles) {
if (Object.prototype.hasOwnProperty.call(styles, property)) {
var prefixedProperty = addVendorPrefix(property);
data.push(prefixedProperty + ":" + styles[property] + ";");
}
}
return data.join('\n');
}
// shorthand for computed style
root.computedStyle = function(element, property, pseudo) {
var prefixedProperty = addVendorPrefix(property);
return window
.getComputedStyle(element, pseudo || null)
.getPropertyValue(prefixedProperty);
};
// flush rendering buffer
root.reflow = function() {
document.body.offsetWidth;
};
// merge objects
root.extend = function(target /*, ..rest */) {
Array.prototype.slice.call(arguments, 1).forEach(function(obj) {
Object.keys(obj).forEach(function(key) {
target[key] = obj[key];
});
});
return target;
};
// dom fixture helper ("resetting dom test elements")
var _domFixture;
var _domFixtureSelector;
root.domFixture = function(selector) {
var fixture = document.querySelector(selector || _domFixtureSelector);
if (!fixture) {
throw new Error('fixture ' + (selector || _domFixtureSelector) + ' not found!');
}
if (!_domFixture && selector) {
// save a copy
_domFixture = fixture.cloneNode(true);
_domFixtureSelector = selector;
} else if (_domFixture) {
// restore the copy
var tmp = _domFixture.cloneNode(true);
fixture.parentNode.replaceChild(tmp, fixture);
} else {
throw new Error('domFixture must be initialized first!');
}
};
root.MS_PER_SEC = 1000;
/*
* The recommended minimum precision to use for time values.
*
* Based on Web Animations:
* https://w3c.github.io/web-animations/#precision-of-time-values
*/
const TIME_PRECISION = 0.0005; // ms
/*
* Allow implementations to substitute an alternative method for comparing
* times based on their precision requirements.
*/
root.assert_times_equal = function(actual, expected, description) {
assert_approx_equals(actual, expected, TIME_PRECISION, description);
};
/*
* Compare a time value based on its precision requirements with a fixed value.
*/
root.assert_time_equals_literal = (actual, expected, description) => {
assert_approx_equals(actual, expected, TIME_PRECISION, description);
};
/**
* Assert that CSSTransition event, |evt|, has the expected property values
* defined by |propertyName|, |elapsedTime|, and |pseudoElement|.
*/
root.assert_end_events_equal = function(evt, propertyName, elapsedTime,
pseudoElement = '') {
assert_equals(evt.propertyName, propertyName);
assert_times_equal(evt.elapsedTime, elapsedTime);
assert_equals(evt.pseudoElement, pseudoElement);
};
/**
* Assert that array of simultaneous CSSTransition events, |evts|, have the
* corresponding property names listed in |propertyNames|, and the expected
* |elapsedTimes| and |pseudoElement| members.
*
* |elapsedTimes| may be a single value if all events are expected to have the
* same elapsedTime, or an array parallel to |propertyNames|.
*/
root.assert_end_event_batch_equal = function(evts, propertyNames, elapsedTimes,
pseudoElement = '') {
assert_equals(
evts.length,
propertyNames.length,
'Test harness error: should have waited for the correct number of events'
);
assert_true(
typeof elapsedTimes === 'number' ||
(Array.isArray(elapsedTimes) &&
elapsedTimes.length === propertyNames.length),
'Test harness error: elapsedTimes must either be a number or an array of' +
' numbers with the same length as propertyNames'
);
if (typeof elapsedTimes === 'number') {
elapsedTimes = Array(propertyNames.length).fill(elapsedTimes);
}
const testPairs = propertyNames.map((propertyName, index) => ({
propertyName,
elapsedTime: elapsedTimes[index]
}));
const sortByPropertyName = (a, b) =>
a.propertyName.localeCompare(b.propertyName);
evts.sort(sortByPropertyName);
testPairs.sort(sortByPropertyName);
for (let evt of evts) {
const expected = testPairs.shift();
assert_end_events_equal(
evt,
expected.propertyName,
expected.elapsedTime,
pseudoElement
);
}
}
/**
* Appends a div to the document body.
*
* @param t The testharness.js Test object. If provided, this will be used
* to register a cleanup callback to remove the div when the test
* finishes.
*
* @param attrs A dictionary object with attribute names and values to set on
* the div.
*/
root.addDiv = function(t, attrs) {
var div = document.createElement('div');
if (attrs) {
for (var attrName in attrs) {
div.setAttribute(attrName, attrs[attrName]);
}
}
document.body.appendChild(div);
if (t && typeof t.add_cleanup === 'function') {
t.add_cleanup(function() {
if (div.parentNode) {
div.remove();
}
});
}
return div;
};
/**
* Appends a style div to the document head.
*
* @param t The testharness.js Test object. If provided, this will be used
* to register a cleanup callback to remove the style element
* when the test finishes.
*
* @param rules A dictionary object with selector names and rules to set on
* the style sheet.
*/
root.addStyle = (t, rules) => {
const extraStyle = document.createElement('style');
document.head.appendChild(extraStyle);
if (rules) {
const sheet = extraStyle.sheet;
for (const selector in rules) {
sheet.insertRule(selector + '{' + rules[selector] + '}',
sheet.cssRules.length);
}
}
if (t && typeof t.add_cleanup === 'function') {
t.add_cleanup(() => {
extraStyle.remove();
});
}
return extraStyle;
};
/**
* Promise wrapper for requestAnimationFrame.
*/
root.waitForFrame = () => {
return new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
};
/**
* Returns a Promise that is resolved after the given number of consecutive
* animation frames have occured (using requestAnimationFrame callbacks).
*
* @param frameCount The number of animation frames.
* @param onFrame An optional function to be processed in each animation frame.
*/
root.waitForAnimationFrames = (frameCount, onFrame) => {
const timeAtStart = document.timeline.currentTime;
return new Promise(resolve => {
function handleFrame() {
if (onFrame && typeof onFrame === 'function') {
onFrame();
}
if (timeAtStart != document.timeline.currentTime &&
--frameCount <= 0) {
resolve();
} else {
window.requestAnimationFrame(handleFrame); // wait another frame
}
}
window.requestAnimationFrame(handleFrame);
});
};
/**
* Wrapper that takes a sequence of N animations and returns:
*
* Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]);
*/
root.waitForAllAnimations = animations =>
Promise.all(animations.map(animation => animation.ready));
/**
* Utility that takes a Promise and a maximum number of frames to wait and
* returns a new Promise that behaves as follows:
*
* - If the provided Promise resolves _before_ the specified number of frames
* have passed, resolves with the result of the provided Promise.
* - If the provided Promise rejects _before_ the specified number of frames
* have passed, rejects with the error result of the provided Promise.
* - Otherwise, rejects with a 'Timed out' error message. If |message| is
* provided, it will be appended to the error message.
*/
root.frameTimeout = (promiseToWaitOn, framesToWait, message) => {
let framesRemaining = framesToWait;
let aborted = false;
const timeoutPromise = new Promise(function waitAFrame(resolve, reject) {
if (aborted) {
resolve();
return;
}
if (framesRemaining-- > 0) {
requestAnimationFrame(() => {
waitAFrame(resolve, reject);
});
return;
}
let errorMessage = 'Timed out waiting for Promise to resolve';
if (message) {
errorMessage += `: ${message}`;
}
reject(new Error(errorMessage));
});
const wrappedPromiseToWaitOn = promiseToWaitOn.then(result => {
aborted = true;
return result;
});
return Promise.race([timeoutPromise, wrappedPromiseToWaitOn]);
};
})(window);