Index: pkg/polymer/lib/elements/web-animations-js/test/bootstrap.js |
diff --git a/pkg/polymer/lib/elements/web-animations-js/test/bootstrap.js b/pkg/polymer/lib/elements/web-animations-js/test/bootstrap.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..fa953864fd41c0eb39f2beaa77819714080df5da |
--- /dev/null |
+++ b/pkg/polymer/lib/elements/web-animations-js/test/bootstrap.js |
@@ -0,0 +1,1304 @@ |
+/** |
+ * Copyright 2013 Google Inc. All Rights Reserved. |
+ * |
+ * Licensed under the Apache License, Version 2.0 (the "License"); |
+ * you may not use this file except in compliance with the License. |
+ * You may obtain a copy of the License at |
+ * |
+ * http://www.apache.org/licenses/LICENSE-2.0 |
+ * |
+ * Unless required by applicable law or agreed to in writing, software |
+ * distributed under the License is distributed on an "AS IS" BASIS, |
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+ * See the License for the specific language governing permissions and |
+ * limitations under the License. |
+ */ |
+'use strict'; |
+ |
+(function() { |
+ |
+var log_element = document.createElement('pre'); |
+log_element.id = 'debug'; |
+ |
+var log_element_parent = setInterval(function() { |
+ if (log_element.parentNode == null && document.body != null) { |
+ document.body.appendChild(log_element); |
+ clearInterval(log_element_parent); |
+ } |
+}, 100); |
+ |
+function log() { |
+ |
+ var output = []; |
+ for (var i = 0; i < arguments.length; i++) { |
+ if (typeof arguments[i] === "function") { |
+ arguments[i] = '' + arguments[i]; |
+ } |
+ if (typeof arguments[i] === "string") { |
+ var bits = arguments[i].replace(/\s*$/, '').split('\n'); |
+ if (bits.length > 5) { |
+ bits.splice(3, bits.length-5, '...'); |
+ } |
+ output.push(bits.join('\n')); |
+ } else if (typeof arguments[i] === "object" && typeof arguments[i].name === "string") { |
+ output.push('"'+arguments[i].name+'"'); |
+ } else { |
+ output.push(JSON.stringify(arguments[i], undefined, 2)); |
+ } |
+ output.push(' '); |
+ } |
+ log_element.appendChild(document.createTextNode(output.join('') + '\n')); |
+} |
+ |
+var thisScript = document.querySelector("script[src$='bootstrap.js']"); |
+var coverageMode = Boolean(parent.window.__coverage__) || /coverage/.test(window.location.hash); |
+ |
+// Inherit these properties from the parent test-runner if any. |
+window.__resources__ = parent.window.__resources__ || {original: {}}; |
+window.__coverage__ = parent.window.__coverage__; |
+ |
+function getSync(src) { |
+ var xhr = new XMLHttpRequest(); |
+ xhr.open('GET', src, false); |
+ xhr.send(); |
+ if (xhr.responseCode > 400) { |
+ console.error('Error loading ' + src); |
+ return ''; |
+ } |
+ return xhr.responseText; |
+} |
+ |
+function loadScript(src, options) { |
+ // Add changing parameter to prevent script caching. |
+ options = options || {coverage: true}; |
+ if (window.__resources__[src]) { |
+ document.write('<script type="text/javascript">eval(window.__resources__["'+src+'"]);</script>'); |
+ } else if (coverageMode && options.coverage) { |
+ instrument(src); |
+ loadScript(src); |
+ } else { |
+ if (!inExploreMode()) { |
+ src += '?' + getCacheBuster(); |
+ } |
+ document.write('<script type="text/javascript" src="'+ src + '"></script>'); |
+ } |
+} |
+ |
+function loadCSS(src) { |
+ document.write('<link rel="stylesheet" type="text/css" href="' + src + '">'); |
+} |
+ |
+function forEach(array, callback, thisObj) { |
+ for (var i=0; i < array.length; i++) { |
+ if (array.hasOwnProperty(i)) { |
+ callback.call(thisObj, array[i], i, array); |
+ } |
+ } |
+} |
+ |
+function hasFlag(flag) { |
+ return thisScript && thisScript.getAttribute(flag) !== null; |
+} |
+ |
+function testType() { |
+ var p = location.pathname; |
+ p = p.replace(/^disabled-/, ''); |
+ |
+ var match = /(auto|impl|manual|unit)-test[^\\\/]*$/.exec(p); |
+ return match ? match[1]: 'unknown'; |
+} |
+ |
+function inExploreMode() { |
+ return '#explore' == window.location.hash || window.location.hash.length == 0; |
+} |
+ |
+/** |
+ * Get a value for busting the cache. If we got given a cache buster, pass it |
+ * along, otherwise generate a new one. |
+ */ |
+var cacheBusterValue = '' + window.Date.now(); |
+function getCacheBuster() { |
+ if (window.location.search.length > 0) |
+ cacheBusterValue = window.location.search.substr(1, window.location.search.length); |
+ return cacheBusterValue; |
+} |
+ |
+var instrumentationDepsLoaded = false; |
+/** |
+ * Instrument the source at {@code location} and store it in |
+ * {@code window.__resources__[name]}. |
+ */ |
+function instrument(src) { |
+ if (__resources__[src]) { |
+ return; |
+ } |
+ if (!instrumentationDepsLoaded) { |
+ instrumentationDepsLoaded = true; |
+ (function() { |
+ eval(getSync('../coverage/esprima/esprima.js')); |
+ eval(getSync('../coverage/escodegen/escodegen.browser.js')); |
+ eval(getSync('../coverage/istanbul/lib/instrumenter.js')); |
+ }).call(window); |
+ } |
+ var js = getSync(src); |
+ window.__resources__.original[src] = js; |
+ var inst = window.__resources__[src] = new Instrumenter().instrumentSync(js, src); |
+} |
+ |
+ |
+var svg_properties = { |
+ cx: 1, |
+ width: 1, |
+ x: 1, |
+ y: 1 |
+}; |
+ |
+var is_svg_attrib = function(property, target) { |
+ return target.namespaceURI == 'http://www.w3.org/2000/svg' && |
+ property in svg_properties; |
+}; |
+ |
+var svg_namespace_uri = 'http://www.w3.org/2000/svg'; |
+ |
+window.test_features = (function() { |
+ var style = document.createElement('style'); |
+ style.textContent = '' + |
+ 'dummyRuleForTesting {' + |
+ 'width: calc(0px);' + |
+ 'width: -webkit-calc(0px); }'; |
+ document.head.appendChild(style); |
+ var transformCandidates = [ |
+ 'transform', |
+ 'webkitTransform', |
+ 'msTransform' |
+ ]; |
+ var transformProperty = transformCandidates.filter(function(property) { |
+ return property in style.sheet.cssRules[0].style; |
+ })[0]; |
+ var calcFunction = style.sheet.cssRules[0].style.width.split('(')[0]; |
+ document.head.removeChild(style); |
+ return { |
+ transformProperty: transformProperty, |
+ calcFunction: calcFunction |
+ }; |
+})(); |
+ |
+/** |
+ * Figure out a useful name for an element. |
+ * |
+ * @param {Element} element Element to get the name for. |
+ * |
+ * @private |
+ */ |
+function _element_name(element) { |
+ if (element.id) { |
+ return element.tagName.toLowerCase() + '#' + element.id; |
+ } else { |
+ return 'An anonymous ' + element.tagName.toLowerCase(); |
+ } |
+} |
+ |
+/** |
+ * Get the style for a given element. |
+ * |
+ * @param {Array.<Object.<string, string>>|Object.<string, string>} style |
+ * Either; |
+ * * A list of dictionaries, each node returned is checked against the |
+ * associated dictionary, or |
+ * * A single dictionary, each node returned is checked against the |
+ * given dictionary. |
+ * Each dictionary should be of the form {style_name: style_value}. |
+ * |
+ * @private |
+ */ |
+function _assert_style_get(style, i) { |
+ if (typeof style[i] === 'undefined') { |
+ return style; |
+ } else { |
+ return style[i]; |
+ } |
+} |
+ |
+/** |
+ * Extract all the informative parts of a string. Ignores spacing, punctuation |
+ * and other random extra characters. |
+ */ |
+function _extract_important(input) { |
+ var re = /([-+]?[0-9]+\.?[0-9]*(?:[eE][-+]?[0-9]+)?)|[A-Za-z%]+/g; |
+ |
+ var match; |
+ var result = []; |
+ while (match = re.exec(input)) { |
+ var value = match[0]; |
+ if (typeof match[1] != "undefined") { |
+ value = Number(match[1]); |
+ } |
+ result.push(value); |
+ } |
+ return result; |
+} |
+window.assert_styles_extract_important = _extract_important; |
+ |
+function AssertionError(message) { |
+ this.message = message; |
+} |
+window.assert_styles_assertion_error = AssertionError; |
+ |
+/** |
+ * Asserts that a string is in the array of expected only comparing the |
+ * important parts. Ignores spacing, punctuation and other random extra |
+ * characters. |
+ */ |
+function _assert_important_in_array(actual, expected, message) { |
+ var actual_array = _extract_important(actual); |
+ |
+ var expected_array_array = []; |
+ for (var i = 0; i < expected.length; i++) { |
+ expected_array_array.push(_extract_important(expected[i])); |
+ } |
+ |
+ var errors = []; |
+ for (var i = 0; i < expected_array_array.length; i++) { |
+ var expected_array = expected_array_array[i]; |
+ |
+ var element_errors = []; |
+ if (actual_array.length != expected_array.length) { |
+ element_errors.push('Number of elements don\'t match'); |
+ } |
+ |
+ for (var j = 0; j < expected_array.length; j++) { |
+ var actual = actual_array[j]; |
+ var expected = expected_array[j]; |
+ |
+ try { |
+ assert_equals(typeof actual, typeof expected); |
+ |
+ if (typeof actual === 'number') { |
+ if (Math.abs(actual) < 1e-10) { |
+ actual = 0; |
+ } |
+ actual = '' + actual.toPrecision(4); |
+ } |
+ if (typeof expected === 'number') { |
+ if (Math.abs(expected) < 1e-10) { |
+ expected = 0; |
+ } |
+ expected = '' + expected.toPrecision(4); |
+ } |
+ |
+ assert_equals(actual, expected); |
+ } catch (e) { |
+ element_errors.push( |
+ 'Element ' + j + ' - ' + e.message); |
+ } |
+ } |
+ |
+ if (element_errors.length == 0) { |
+ return; |
+ } else { |
+ errors.push( |
+ ' Expectation ' + JSON.stringify(expected_array) + ' did not match\n' + |
+ ' ' + element_errors.join('\n ')); |
+ } |
+ } |
+ if (expected_array_array.length > 1) |
+ errors.unshift(' ' + expected_array_array.length + ' possible expectations'); |
+ |
+ errors.unshift(' Actual - ' + JSON.stringify(actual_array)); |
+ if (typeof message !== 'undefined') { |
+ errors.unshift(message); |
+ } |
+ throw new AssertionError(errors.join('\n')); |
+} |
+window.assert_styles_assert_important_in_array = _assert_important_in_array; |
+ |
+/** |
+ * asserts that actual has the same styles as the dictionary given by |
+ * expected. |
+ * |
+ * @param {Element} object DOM node to check the styles on |
+ * @param {Object.<string, string>} styles Dictionary of {style_name: style_value} to check |
+ * on the object. |
+ * @param {String} description Human readable description of what you are |
+ * trying to check. |
+ * |
+ * @private |
+ */ |
+function _assert_style_element(object, style, description) { |
+ if (typeof message == 'undefined') |
+ description = ''; |
+ |
+ // Create an element of the same type as testing so the style can be applied |
+ // from the test. This is so the css property (not the -webkit-does-something |
+ // tag) can be read. |
+ var reference_element = (object.namespaceURI == svg_namespace_uri) ? |
+ document.createElementNS(svg_namespace_uri, object.nodeName) : |
+ document.createElement(object.nodeName); |
+ var computedObjectStyle = getComputedStyle(object, null); |
+ for (var i = 0; i < computedObjectStyle.length; i++) { |
+ var property = computedObjectStyle[i]; |
+ reference_element.style.setProperty(property, |
+ computedObjectStyle.getPropertyValue(property)); |
+ } |
+ reference_element.style.position = 'absolute'; |
+ if (object.parentNode) { |
+ object.parentNode.appendChild(reference_element); |
+ } |
+ |
+ try { |
+ // Apply the style |
+ for (var prop_name in style) { |
+ // If the passed in value is an element then grab its current style for |
+ // that property |
+ if (style[prop_name] instanceof HTMLElement || |
+ style[prop_name] instanceof SVGElement) { |
+ |
+ var prop_value = getComputedStyle(style[prop_name], null)[prop_name]; |
+ } else { |
+ var prop_value = style[prop_name]; |
+ } |
+ |
+ prop_value = '' + prop_value; |
+ |
+ if (prop_name == 'transform') { |
+ var output_prop_name = test_features.transformProperty; |
+ } else { |
+ var output_prop_name = prop_name; |
+ } |
+ |
+ var is_svg = is_svg_attrib(prop_name, object); |
+ if (is_svg) { |
+ reference_element.setAttribute(prop_name, prop_value); |
+ |
+ var current_style = object.attributes; |
+ var target_style = reference_element.attributes; |
+ } else { |
+ reference_element.style[output_prop_name] = prop_value; |
+ |
+ var current_style = computedObjectStyle; |
+ var target_style = getComputedStyle(reference_element, null); |
+ |
+ _assert_important_in_array( |
+ prop_value, [reference_element.style[output_prop_name], target_style[output_prop_name]], |
+ 'Tried to set the reference element\'s '+ output_prop_name + |
+ ' to ' + JSON.stringify(prop_value) + |
+ ' but neither the style' + |
+ ' ' + JSON.stringify(reference_element.style[output_prop_name]) + |
+ ' nor computedStyle ' + JSON.stringify(target) + |
+ ' ended up matching requested value.'); |
+ } |
+ |
+ if (prop_name == 'ctm') { |
+ var ctm = object.getCTM(); |
+ var curr = '{' + ctm.a + ', ' + |
+ ctm.b + ', ' + ctm.c + ', ' + ctm.d + ', ' + |
+ ctm.e + ', ' + ctm.f + '}'; |
+ |
+ var target = prop_value; |
+ |
+ } else if (is_svg) { |
+ var target = target_style[prop_name].value; |
+ var curr = current_style[prop_name].value; |
+ } else { |
+ var target = target_style[output_prop_name]; |
+ var curr = current_style[output_prop_name]; |
+ } |
+ |
+ var description_extra = '\n Property ' + prop_name; |
+ if (prop_name != output_prop_name) |
+ description_extra += '(actually ' + output_prop_name + ')'; |
+ |
+ _assert_important_in_array(curr, [target], description + description_extra); |
+ } |
+ } finally { |
+ if (reference_element.parentNode) { |
+ reference_element.parentNode.removeChild(reference_element); |
+ } |
+ } |
+} |
+ |
+/** |
+ * asserts that elements in the list have given styles. |
+ * |
+ * @param {Array.<Element>} objects List of DOM nodes to check the styles on |
+ * @param {Array.<Object.<string, string>>|Object.<string, string>} style |
+ * See _assert_style_get for information. |
+ * @param {String} description Human readable description of what you are |
+ * trying to check. |
+ * |
+ * @private |
+ */ |
+function _assert_style_element_list(objects, style, description) { |
+ var error = ''; |
+ forEach(objects, function(object, i) { |
+ try { |
+ _assert_style_element( |
+ object, _assert_style_get(style, i), |
+ description + ' ' + _element_name(object) |
+ ); |
+ } catch (e) { |
+ if (error) { |
+ error += '; '; |
+ } |
+ error += 'Element ' + _element_name(object) + ' at index ' + i + ' failed ' + e.message + '\n'; |
+ } |
+ }); |
+ if (error) { |
+ throw error; |
+ } |
+} |
+ |
+/** |
+ * asserts that elements returned from a query selector have a list of styles. |
+ * |
+ * @param {string} qs A query selector to use to get the DOM nodes. |
+ * @param {Array.<Object.<string, string>>|Object.<string, string>} style |
+ * See _assert_style_get for information. |
+ * @param {String} description Human readable description of what you are |
+ * trying to check. |
+ * |
+ * @private |
+ */ |
+function _assert_style_queryselector(qs, style, description) { |
+ var objects = document.querySelectorAll(qs); |
+ assert_true(objects.length > 0, description + |
+ ' is invalid, no elements match query selector: ' + qs); |
+ _assert_style_element_list(objects, style, description); |
+} |
+ |
+/** |
+ * asserts that elements returned from a query selector have a list of styles. |
+ * |
+ * Assert the element with id #hello is 100px wide; |
+ * assert_styles(document.getElementById('hello'), {'width': '100px'}) |
+ * assert_styles('#hello'), {'width': '100px'}) |
+ * |
+ * Assert all divs are 100px wide; |
+ * assert_styles(document.getElementsByTagName('div'), {'width': '100px'}) |
+ * assert_styles('div', {'width': '100px'}) |
+ * |
+ * Assert all objects with class 'red' are 100px wide; |
+ * assert_styles(document.getElementsByClassName('red'), {'width': '100px'}) |
+ * assert_styles('.red', {'width': '100px'}) |
+ * |
+ * Assert first div is 100px wide, second div is 200px wide; |
+ * assert_styles(document.getElementsByTagName('div'), |
+ * [{'width': '100px'}, {'width': '200px'}]) |
+ * assert_styles('div', |
+ * [{'width': '100px'}, {'width': '200px'}]) |
+ * |
+ * @param {string|Element|Array.<Element>} objects Either; |
+ * * A query selector to use to get DOM nodes, |
+ * * A DOM node. |
+ * * A list of DOM nodes. |
+ * @param {Array.<Object.<string, string>>|Object.<string, string>} style |
+ * See _assert_style_get for information. |
+ */ |
+function assert_styles(objects, style, description) { |
+ switch (typeof objects) { |
+ case 'string': |
+ _assert_style_queryselector(objects, style, description); |
+ break; |
+ |
+ case 'object': |
+ if (objects instanceof Array || objects instanceof NodeList) { |
+ _assert_style_element_list(objects, style, description); |
+ } else if (objects instanceof Element) { |
+ _assert_style_element(objects, style, description); |
+ } else { |
+ throw new Error('Expected Array, NodeList or Element but got ' + objects); |
+ } |
+ break; |
+ } |
+} |
+window.assert_styles = assert_styles; |
+ |
+/** |
+ * Schedule something to be called at a given time. |
+ * |
+ * @constructor |
+ * @param {number} millis Microseconds after start at which the callback should |
+ * be called. |
+ * @param {bool} autostart Auto something... |
+ */ |
+function TestTimelineGroup(millis) { |
+ this.millis = millis; |
+ |
+ /** |
+ * @type {bool} |
+ */ |
+ this.autorun_ = false; |
+ |
+ /** |
+ * @type {!Array.<function(): ?Object>} |
+ */ |
+ this.startCallbacks = null; |
+ |
+ /** |
+ * Callbacks which are added after the timeline has started. We clear them |
+ * when going backwards. |
+ * |
+ * @type {?Array.<function(): ?Object>} |
+ */ |
+ this.lateCallbacks = null; |
+ |
+ /** |
+ * @type {Element} |
+ */ |
+ this.marker = document.createElement('img'); |
+ /** |
+ * @type {Element} |
+ */ |
+ this.info = document.createElement('div'); |
+ |
+ this.setup_(); |
+} |
+ |
+TestTimelineGroup.prototype.setup_ = function() { |
+ this.endTime_ = 0; |
+ this.startCallbacks = new Array(); |
+ this.lateCallbacks = null; |
+ this.marker.innerHTML = ''; |
+ this.info.innerHTML = ''; |
+}; |
+ |
+/** |
+ * Add a new callback to the event group |
+ * |
+ * @param {function(): ?Object} callback Callback given the currentTime of |
+ * callback. |
+ */ |
+TestTimelineGroup.prototype.add = function(callback) { |
+ if (this.lateCallbacks === null) { |
+ this.startCallbacks.unshift(callback); |
+ } else { |
+ this.lateCallbacks.unshift(callback); |
+ } |
+ |
+ // Trim out extra 'function() { ... }' |
+ var callbackString = callback.name; |
+ // FIXME: This should probably unindent too.... |
+ this.info.innerHTML += '<div>' + callbackString + '</div>'; |
+}; |
+ |
+/** |
+ * Reset this event group to the state before start was called. |
+ */ |
+TestTimelineGroup.prototype.reset = function() { |
+ this.lateCallbacks = null; |
+ |
+ var callbacks = this.startCallbacks.slice(0); |
+ this.setup_(); |
+ while (callbacks.length > 0) { |
+ var callback = callbacks.shift(); |
+ this.add(callback); |
+ } |
+}; |
+ |
+/** |
+ * Tell the event group that the timeline has started and that any callbacks |
+ * added from now are dynamically generated and hence should be cleared when a |
+ * reset is called. |
+ */ |
+TestTimelineGroup.prototype.start = function() { |
+ this.lateCallbacks = new Array(); |
+}; |
+ |
+/** |
+ * Call all the callbacks in the EventGroup. |
+ */ |
+TestTimelineGroup.prototype.call = function() { |
+ var callbacks = (this.startCallbacks.slice(0)).concat(this.lateCallbacks); |
+ var statuses = this.info.children; |
+ |
+ var overallResult = true; |
+ while (callbacks.length > 0) { |
+ var callback = callbacks.pop(); |
+ |
+ var status_ = statuses[statuses.length - callbacks.length - 1]; |
+ |
+ if (typeof callback == 'function') { |
+ log('TestTimelineGroup', 'calling function', callback); |
+ try { |
+ callback(); |
+ } catch (e) { |
+ // On IE the only way to get the real stack is to do this |
+ window.onerror(e.message, e.fileName, e.lineNumber, e); |
+ // On other browsers we want to throw the error later |
+ setTimeout(function () { throw e; }, 0); |
+ } |
+ } else { |
+ log('TestTimelineGroup', 'calling test', callback); |
+ var result = callback.step(callback.f); |
+ callback.done(); |
+ } |
+ |
+ if (result === undefined || result == null) { |
+ overallResult = overallResult && true; |
+ |
+ status_.style.color = 'green'; |
+ } else { |
+ overallResult = overallResult && false; |
+ status_.style.color = 'red'; |
+ status_.innerHTML += '<div>' + result.toString() + '</div>'; |
+ } |
+ } |
+ if (overallResult) { |
+ this.marker.src = '../img/success.png'; |
+ } else { |
+ this.marker.src = '../img/error.png'; |
+ } |
+} |
+ |
+/** |
+ * Draw the EventGroup's marker at the correct position on the timeline. |
+ * |
+ * FIXME(mithro): This mixes display and control :( |
+ * |
+ * @param {number} endTime The endtime of the timeline in millis. Used to |
+ * display the marker at the right place on the timeline. |
+ */ |
+TestTimelineGroup.prototype.draw = function(container, endTime) { |
+ this.marker.title = this.millis + 'ms'; |
+ this.marker.className = 'marker'; |
+ this.marker.src = '../img/unknown.png'; |
+ |
+ var mleft = 'calc(100% - 10px)'; |
+ if (endTime != 0) { |
+ mleft = 'calc(' + (this.millis / endTime) * 100.0 + '%' + ' - 10px)'; |
+ } |
+ this.marker.style.left = mleft; |
+ |
+ container.appendChild(this.marker); |
+ |
+ this.info.className = 'info'; |
+ container.appendChild(this.info); |
+ |
+ // Display details about the events at this time period when hovering over |
+ // the marker. |
+ this.marker.onmouseover = function() { |
+ this.style.display = 'block'; |
+ }.bind(this.info); |
+ |
+ this.marker.onmouseout = function() { |
+ this.style.display = 'none'; |
+ }.bind(this.info); |
+ |
+ |
+ var offset = Math.ceil(this.info.offsetWidth / 2); |
+ var ileft = 'calc(100% - ' + offset + 'px)'; |
+ if (endTime != 0) { |
+ ileft = 'calc(' + (this.millis / endTime) * 100.0 + '%' + ' - ' + offset + |
+ 'px)'; |
+ } |
+ this.info.style.left = ileft; |
+ |
+ this.info.style.display = 'none'; |
+}; |
+ |
+ |
+ |
+/** |
+ * Moves the testharness_timeline in "real time". |
+ * (IE 1 test second takes 1 real second). |
+ * |
+ * @constructor |
+ */ |
+function RealtimeRunner(timeline) { |
+ this.timeline = timeline; |
+ |
+ // Capture the real requestAnimationFrame so we can run in 'real time' mode |
+ // rather than as fast as possible. |
+ var nativeRequestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame; |
+ this.boundRequestAnimationFrame = function(f) { |
+ nativeRequestAnimationFrame(f.bind(this)) |
+ }; |
+ this.now = window.Date.now; |
+ |
+ this.zeroTime = null; // Time the page loaded |
+ this.pauseStartTime = null; // Time at which we paused raf |
+ this.timeDrift = 0; // Amount we have been stopped for |
+} |
+ |
+/** |
+ * Callback called from nativeRequestAnimationFrame. |
+ * |
+ * @private |
+ * @param {number} timestamp The current time for the animation frame |
+ * (in millis). |
+ */ |
+RealtimeRunner.prototype.animationFrame_ = function(timestamp) { |
+ if (this.zeroTime === null) { |
+ this.zeroTime = timestamp; |
+ } |
+ |
+ // Are we paused? Stop calling requestAnimationFrame. |
+ if (this.pauseStartTime != null) { |
+ return; |
+ } |
+ |
+ var virtualAnimationTime = timestamp - this.zeroTime - this.timeDrift; |
+ var endTime = this.timeline.endTime_; |
+ // If we have no events paste t=0, endTime is going to be zero. Instead |
+ // make the test run for 2 minutes. |
+ if (endTime == 0) { |
+ endTime = 120e3; |
+ } |
+ |
+ // Do we still have time to go? |
+ if (virtualAnimationTime < endTime) { |
+ try { |
+ this.timeline.setTime(virtualAnimationTime); |
+ } finally { |
+ this.boundRequestAnimationFrame(this.animationFrame_); |
+ } |
+ |
+ } else { |
+ // Have we gone past endTime_? Force the harness to its endTime_. |
+ |
+ this.timeline.setTime(endTime); |
+ // Don't continue to raf |
+ } |
+}; |
+ |
+RealtimeRunner.prototype.start = function() { |
+ if (this.pauseStartTime != null) { |
+ this.timeDrift += (this.now() - this.pauseStartTime); |
+ this.pauseStartTime = null; |
+ } |
+ this.boundRequestAnimationFrame(this.animationFrame_); |
+}; |
+ |
+RealtimeRunner.prototype.pause = function() { |
+ if (this.pauseStartTime != null) { |
+ return; |
+ } |
+ this.pauseStartTime = this.now(); |
+}; |
+ |
+ |
+/** |
+ * Class for storing events that happen during at given times (such as |
+ * animation checks, or setTimeout). |
+ * |
+ * @constructor |
+ */ |
+function TestTimeline(everyFrame) { |
+ log('TestTimeline', 'constructor', everyFrame); |
+ /** |
+ * Stores the events which are upcoming. |
+ * |
+ * @type Object.<number, TestTimelineGroup> |
+ * @private |
+ */ |
+ this.timeline_ = new Array(); |
+ |
+ this.everyFrame = everyFrame; |
+ this.frameMillis = 1000.0 / 60; //60fps |
+ |
+ this.currentTime_ = -this.frameMillis; |
+ |
+ // Schedule an event at t=0, needed temporarily. |
+ this.schedule(function() {}, 0); |
+ |
+ this.reset(); |
+ |
+ this.runner_ = new RealtimeRunner(this); |
+} |
+ |
+/** |
+ * Create the GUI controller for the timeline. |
+ * @param {Element} body DOM element to add the GUI too, normally the <body> |
+ * element. |
+ */ |
+TestTimeline.prototype.createGUI = function(body) { |
+ // HTML needed to create the timeline UI |
+ this.div = document.createElement('div'); |
+ this.div.id = 'timeline'; |
+ |
+ this.timelinebar = document.createElement('div'); |
+ this.timelinebar.className = 'bar'; |
+ |
+ this.timelineprogress = document.createElement('div'); |
+ this.timelineprogress.className = 'progress'; |
+ |
+ this.timelinebar.appendChild(this.timelineprogress); |
+ this.div.appendChild(this.timelinebar); |
+ |
+ this.next = document.createElement('button'); |
+ this.next.innerText = '>'; |
+ this.next.id = 'next'; |
+ this.next.onclick = this.toNextEvent.bind(this); |
+ this.div.appendChild(this.next); |
+ |
+ this.prev = document.createElement('button'); |
+ this.prev.innerText = '<'; |
+ this.prev.id = 'prev'; |
+ this.prev.onclick = this.toPrevEvent.bind(this); |
+ this.div.appendChild(this.prev); |
+ |
+ this.control = document.createElement('button'); |
+ this.control.innerText = 'Pause'; |
+ this.control.id = 'control'; |
+ this.control.onclick = function() { |
+ if (this.control.innerText == 'Go!') { |
+ this.runner_.start(); |
+ this.control.innerText = 'Pause'; |
+ } else { |
+ this.runner_.pause(); |
+ this.control.innerText = 'Go!'; |
+ } |
+ }.bind(this); |
+ this.div.appendChild(this.control); |
+ |
+ body.appendChild(this.div); |
+} |
+ |
+/** |
+ * Update GUI elements. |
+ * |
+ * @private |
+ */ |
+TestTimeline.prototype.updateGUI = function () { |
+ // Update the timeline |
+ var width = "100%"; |
+ if (this.endTime_ != 0) { |
+ width = (this.currentTime_ / this.endTime_) * 100.0 +'%' |
+ } |
+ this.timelineprogress.style.width = width; |
+ this.timelinebar.title = (this.currentTime_).toFixed(0) + 'ms'; |
+}; |
+ |
+ |
+/** |
+ * Sort the timeline into run order. Should be called after adding something to |
+ * the timeline. |
+ * |
+ * @private |
+ */ |
+TestTimeline.prototype.sort_ = function() { |
+ this.timeline_.sort(function(a,b) { |
+ return a.millis - b.millis; |
+ }); |
+}; |
+ |
+/** |
+ * Schedule something to be called at a given time. |
+ * |
+ * @param {function(number)} callback Callback to call after the number of millis |
+ * have elapsed. |
+ * @param {number} millis Milliseconds after start at which the callback should |
+ * be called. |
+ */ |
+TestTimeline.prototype.schedule = function(callback, millis) { |
+ log('TestTimeline', 'schedule', millis, callback); |
+ if (millis < this.currentTime_) { |
+ // Can't schedule something in the past? |
+ return; |
+ } |
+ |
+ // See if there is something at that time in the timeline already? |
+ var timeline = this.timeline_.slice(0); |
+ var group = null; |
+ while (timeline.length > 0) { |
+ if (timeline[0].millis == millis) { |
+ group = timeline[0]; |
+ break; |
+ } else { |
+ timeline.shift(); |
+ } |
+ } |
+ |
+ // If not, create a node at that time. |
+ if (group === null) { |
+ group = new TestTimelineGroup(millis); |
+ this.timeline_.unshift(group); |
+ this.sort_(); |
+ } |
+ group.add(callback); |
+ |
+ var newEndTime = this.timeline_.slice(-1)[0].millis * 1.1; |
+ if (this.endTime_ != newEndTime) { |
+ this.endTime_ = newEndTime; |
+ } |
+}; |
+ |
+/** |
+ * Return the current time in milliseconds. |
+ */ |
+TestTimeline.prototype.now = function() { |
+ log('TestTimeline', 'now', Math.max(this.currentTime_, 0)); |
+ return Math.max(this.currentTime_, 0); |
+}; |
+ |
+/** |
+ * Set the current time to a given value. |
+ * |
+ * @param {number} millis Time in milliseconds to set the current time too. |
+ */ |
+TestTimeline.prototype.setTime = function(millis) { |
+ log('TestTimeline', 'setTime', millis); |
+ // Time is going backwards, we actually have to reset and go forwards as |
+ // events can cause the creation of more events. |
+ if (this.currentTime_ > millis) { |
+ this.reset(); |
+ this.start(); |
+ } |
+ |
+ var events = this.timeline_.slice(0); |
+ |
+ // Already processed events |
+ while (events.length > 0 && events[0].millis <= this.currentTime_) { |
+ events.shift(); |
+ } |
+ |
+ while (this.currentTime_ < millis) { |
+ var event_ = null; |
+ var moveTo = millis; |
+ |
+ if (events.length > 0 && events[0].millis <= millis) { |
+ event_ = events.shift(); |
+ moveTo = event_.millis; |
+ } |
+ |
+ // Call the callback |
+ if (this.currentTime_ != moveTo) { |
+ log('TestTimeline', 'setting time to', moveTo); |
+ this.currentTime_ = moveTo; |
+ this.animationFrame(this.currentTime_); |
+ } |
+ |
+ if (event_) { |
+ event_.call(); |
+ } |
+ } |
+ |
+ this.updateGUI(); |
+ |
+ if (millis >= this.endTime_) { |
+ this.done(); |
+ } |
+}; |
+ |
+/** |
+ * Call all callbacks registered for the next (virtual) animation frame. |
+ * |
+ * @param {number} millis Time in milliseconds. |
+ * @private |
+ */ |
+TestTimeline.prototype.animationFrame = function(millis) { |
+ /* FIXME(mithro): Code should appear here to allow testing of running |
+ * every animation frame. |
+ |
+ if (this.everyFrame) { |
+ } |
+ |
+ */ |
+ |
+ var callbacks = this.animationFrameCallbacks; |
+ callbacks.reverse(); |
+ this.animationFrameCallbacks = []; |
+ for (var i = 0; i < callbacks.length; i++) { |
+ log('TestTimeline raf callback', callbacks[i], millis); |
+ try { |
+ callbacks[i](millis); |
+ } catch (e) { |
+ // On IE the only way to get the real stack is to do this |
+ window.onerror(e.message, e.fileName, e.lineNumber, e); |
+ // On other browsers we want to throw the error later |
+ setTimeout(function () { throw e; }, 0); |
+ } |
+ } |
+}; |
+ |
+/** |
+ * Set a callback to run at the next (virtual) animation frame. |
+ * |
+ * @param {function(millis)} millis Time in milliseconds to set the current |
+ * time too. |
+ */ |
+TestTimeline.prototype.requestAnimationFrame = function(callback) { |
+ // FIXME: This should return a reference that allows people to cancel the |
+ // animationFrame callback. |
+ this.animationFrameCallbacks.push(callback); |
+ return -1; |
+}; |
+ |
+/** |
+ * Go to next scheduled event in timeline. |
+ */ |
+TestTimeline.prototype.toNextEvent = function() { |
+ var events = this.timeline_.slice(0); |
+ while (events.length > 0 && events[0].millis <= this.currentTime_) { |
+ events.shift(); |
+ } |
+ if (events.length > 0) { |
+ this.setTime(events[0].millis); |
+ |
+ if (this.autorun_) { |
+ setTimeout(this.toNextEvent.bind(this), 0); |
+ } |
+ |
+ return true; |
+ } else { |
+ this.setTime(this.endTime_); |
+ return false; |
+ } |
+ |
+}; |
+ |
+/** |
+ * Go to previous scheduled event in timeline. |
+ * (This actually goes back to time zero and then forward to this event.) |
+ */ |
+TestTimeline.prototype.toPrevEvent = function() { |
+ var events = this.timeline_.slice(0); |
+ while (events.length > 0 && |
+ events[events.length - 1].millis >= this.currentTime_) { |
+ events.pop(); |
+ } |
+ if (events.length > 0) { |
+ this.setTime(events[events.length - 1].millis); |
+ return true; |
+ } else { |
+ this.setTime(0); |
+ return false; |
+ } |
+}; |
+ |
+/** |
+ * Reset the timeline to time zero. |
+ */ |
+TestTimeline.prototype.reset = function () { |
+ for (var t in this.timeline_) { |
+ this.timeline_[t].reset(); |
+ } |
+ |
+ this.currentTime_ = -this.frameMillis; |
+ this.animationFrameCallbacks = []; |
+ this.started_ = false; |
+}; |
+ |
+/** |
+ * Call to initiate starting??? |
+ */ |
+TestTimeline.prototype.start = function () { |
+ this.started_ = true; |
+ |
+ var parent = this; |
+ |
+ for (var t in this.timeline_) { |
+ this.timeline_[t].start(); |
+ // FIXME(mithro) this is confusing... |
+ this.timeline_[t].draw(this.timelinebar, this.endTime_); |
+ |
+ this.timeline_[t].marker.onclick = function(event) { |
+ parent.setTime(this.millis); |
+ event.stopPropagation(); |
+ }.bind(this.timeline_[t]); |
+ } |
+ |
+ this.timelinebar.onclick = function(evt) { |
+ var setPercent = |
+ ((evt.clientX - this.offsetLeft) / this.offsetWidth); |
+ parent.setTime(setPercent * parent.endTime_); |
+ }.bind(this.timelinebar); |
+}; |
+ |
+TestTimeline.prototype.done = function () { |
+ log('TestTime', 'done'); |
+ done(); |
+}; |
+ |
+TestTimeline.prototype.autorun = function() { |
+ this.autorun_ = true; |
+ this.toNextEvent(); |
+}; |
+ |
+function testharness_timeline_setup() { |
+ log('testharness_timeline_setup'); |
+ testharness_timeline.createGUI(document.getElementsByTagName('body')[0]); |
+ testharness_timeline.start(); |
+ testharness_timeline.updateGUI(); |
+ |
+ // Start running the test on message |
+ if ('#message' == window.location.hash) { |
+ window.addEventListener('message', function(evt) { |
+ switch (evt.data['type']) { |
+ case 'start': |
+ if (evt.data['url'] == window.location.href) { |
+ testharness_timeline.autorun(); |
+ } |
+ break; |
+ } |
+ }); |
+ } else if ('#auto' == window.location.hash || '#coverage' == window.location.hash) { |
+ // Run the test as fast as possible, skipping time. |
+ |
+ // Need non-zero timeout to allow chrome to run other code. |
+ setTimeout(testharness_timeline.autorun.bind(testharness_timeline), 1); |
+ |
+ } else if (inExploreMode()) { |
+ setTimeout(testharness_timeline.runner_.start.bind(testharness_timeline.runner_), 1); |
+ } else { |
+ alert('Unknown start mode.'); |
+ } |
+} |
+ |
+// Capture testharness's test as we are about to screw with it. |
+var testharness_test = window.test; |
+ |
+function override_at(replacement_at, f, args) { |
+ var orig_at = window.at; |
+ window.at = replacement_at; |
+ f.apply(null, args); |
+ window.at = orig_at; |
+} |
+ |
+function timing_test(f, desc) { |
+ /** |
+ * at function inside a timing_test function allows testing things at a |
+ * given time rather then onload. |
+ * @param {number} seconds Seconds after page load to run the tests. |
+ * @param {function()} f Closure containing the asserts to be run. |
+ * @param {string} desc Description |
+ */ |
+ var at = function(seconds, f, desc_at) { |
+ assert_true(typeof seconds == 'number', "at's first argument shoud be a number."); |
+ assert_true(!isNaN(seconds), "at's first argument should be a number not NaN!"); |
+ assert_true(seconds >= 0, "at's first argument should be greater then 0."); |
+ assert_true(isFinite(seconds), "at's first argument should be finite."); |
+ |
+ assert_true(typeof f == 'function', "at's second argument should be a function."); |
+ |
+ // Deliberately hoist the desc if we where not given one. |
+ if (typeof desc_at == 'undefined' || desc_at == null || desc_at.length == 0) { |
+ desc_at = desc; |
+ } |
+ |
+ // And then provide 'Unnamed' as a default |
+ if (typeof desc_at == 'undefined' || desc_at == null || desc_at.length == 0) { |
+ desc_at = 'Unnamed assert'; |
+ } |
+ |
+ var t = async_test(desc_at + ' at t=' + seconds + 's'); |
+ t.f = f; |
+ window.testharness_timeline.schedule(t, seconds * 1000.0); |
+ }; |
+ override_at(at, f); |
+} |
+ |
+function test_without_at(f, desc) { |
+ // Make sure calling at inside a test() function is a failure. |
+ override_at(function() { |
+ throw {'message': 'Can not use at() inside a test, use a timing_test instead.'}; |
+ }, function() { testharness_test(f, desc); }); |
+} |
+ |
+/** |
+ * at function schedules a to be called at a given point. |
+ * @param {number} seconds Seconds after page load to run the function. |
+ * @param {function()} f Function to be called. Called with no arguments |
+ */ |
+function at(seconds, f) { |
+ assert_true(typeof seconds == 'number', "at's first argument shoud be a number."); |
+ assert_true(typeof f == 'function', "at's second argument should be a function."); |
+ |
+ window.testharness_timeline.schedule(f, seconds * 1000.0); |
+} |
+ |
+window.testharness_after_loaded = function() { |
+ log('testharness_after_loaded'); |
+ /** |
+ * These steps needs to occur after testharness is loaded. |
+ */ |
+ setup(function() {}, { |
+ explicit_timeout: true, |
+ explicit_done: ((typeof window.testharness_timeline) !== 'undefined')}); |
+ |
+ /** |
+ * Create an testharness test which makes sure the page contains no |
+ * javascript errors. This is needed otherwise if the page contains errors |
+ * then preventing the tests loading it will look like it passed. |
+ */ |
+ var pageerror_test = async_test('Page contains no errors'); |
+ |
+ window.onerror = function(msg, url, line, e) { |
+ var msg = '\nError in ' + url + '\n' + |
+ 'Line ' + line + ': ' + msg + '\n'; |
+ |
+ if (typeof e != "undefined") { |
+ msg += e.stack; |
+ } |
+ |
+ pageerror_test.is_done = true; |
+ pageerror_test.step(function() { |
+ throw new AssertionError(msg); |
+ }); |
+ pageerror_test.is_done = false; |
+ }; |
+ |
+ var pageerror_tests; |
+ function pageerror_othertests_finished(test, harness) { |
+ if (harness == null && pageerror_tests == null) { |
+ return; |
+ } |
+ |
+ if (pageerror_tests == null) { |
+ pageerror_tests = harness; |
+ } |
+ |
+ if (pageerror_tests.all_loaded && pageerror_tests.num_pending == 1) { |
+ pageerror_test.done(); |
+ } |
+ } |
+ add_result_callback(pageerror_othertests_finished); |
+ addEventListener('load', pageerror_othertests_finished); |
+ |
+}; |
+ |
+loadScript('../testharness/testharness.js', {coverage: false}); |
+document.write('<script type="text/javascript">window.testharness_after_loaded();</script>'); |
+loadCSS('../testharness/testharness.css'); |
+loadCSS('../testharness_timing.css'); |
+ |
+if (testType() == 'auto') { |
+ var checksFile = location.pathname; |
+ checksFile = checksFile.replace(/disabled-/, ''); |
+ checksFile = checksFile.replace(/.html$/, '-checks.js') |
+ loadScript(checksFile, {coverage: false}); |
+} |
+ |
+document.write('<div id="log"></div>'); |
+loadScript('../testharness/testharnessreport.js', {coverage: false}); |
+ |
+if (!hasFlag('nopolyfill')) { |
+ loadScript('../../web-animations.js'); |
+} |
+ |
+addEventListener('load', function() { |
+ if (window._WebAnimationsTestingUtilities) { |
+ // Currently enabling asserts breaks auto-test-initial in IE. |
+ //_WebAnimationsTestingUtilities._enableAsserts(); |
+ } |
+}); |
+ |
+// Don't export the timing functions in unittests. |
+if (testType() != 'unit') { |
+ addEventListener('load', testharness_timeline_setup); |
+ |
+ window.at = at; |
+ window.timing_test = timing_test; |
+ window.test = test_without_at; |
+ |
+ // Expose the extra API |
+ window.testharness_timeline = new TestTimeline(); |
+ |
+ // Override existing timing functions |
+ window.requestAnimationFrame = |
+ testharness_timeline.requestAnimationFrame.bind(testharness_timeline); |
+ window.performance.now = null; |
+ window.Date.now = testharness_timeline.now.bind(testharness_timeline); |
+} |
+ |
+})(); |