/** * 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(''); } else if (coverageMode && options.coverage) { instrument(src); loadScript(src); } else { if (!inExploreMode()) { src += '?' + getCacheBuster(); } document.write(''); } } function loadCSS(src) { document.write(''); } 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_namespace_uri = 'http://www.w3.org/2000/svg'; /** * 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.} 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.} 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; var output_prop_name = _WebAnimationsTestingUtilities._prefixProperty(prop_name); var is_svg = _WebAnimationsTestingUtilities._propertyIsSVGAttrib(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.} objects List of DOM nodes to check the styles on * @param {Array.>|Object.} 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.} 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.} objects Either; * * A query selector to use to get DOM nodes, * * A DOM node. * * A list of DOM nodes. * @param {Array.>|Object.} 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 Milliseconds 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.} */ this.startCallbacks = null; /** * Callbacks which are added after the timeline has started. We clear them * when going backwards. * * @type {?Array.} */ 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 += '
' + callbackString + '
'; }; /** * 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 += '
' + result.toString() + '
'; } } 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. * @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 * 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} millis Milliseconds after page load to run the tests. * @param {function()} f Closure containing the asserts to be run. * @param {string} desc Description */ var at = function(millis, f, desc_at) { assert_true(typeof millis == 'number', "at's first argument shoud be a number."); assert_true(!isNaN(millis), "at's first argument should be a number not NaN!"); assert_true(millis >= 0, "at's first argument should be greater then 0."); assert_true(isFinite(millis), "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=' + millis + 'ms'); t.f = f; window.testharness_timeline.schedule(t, millis); }; 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} millis Milliseconds after page load to run the function. * @param {function()} f Function to be called. Called with no arguments */ function at(millis, f) { assert_true(typeof millis == '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, millis); } 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(''); 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('
'); 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); } window.inExploreMode = inExploreMode; })();