OLD | NEW |
| (Empty) |
1 /** | |
2 * Copyright 2013 Google Inc. All Rights Reserved. | |
3 * | |
4 * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 * you may not use this file except in compliance with the License. | |
6 * You may obtain a copy of the License at | |
7 * | |
8 * http://www.apache.org/licenses/LICENSE-2.0 | |
9 * | |
10 * Unless required by applicable law or agreed to in writing, software | |
11 * distributed under the License is distributed on an "AS IS" BASIS, | |
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 * See the License for the specific language governing permissions and | |
14 * limitations under the License. | |
15 */ | |
16 'use strict'; | |
17 | |
18 (function() { | |
19 | |
20 var log_element = document.createElement('pre'); | |
21 log_element.id = 'debug'; | |
22 | |
23 var log_element_parent = setInterval(function() { | |
24 if (log_element.parentNode == null && document.body != null) { | |
25 document.body.appendChild(log_element); | |
26 clearInterval(log_element_parent); | |
27 } | |
28 }, 100); | |
29 | |
30 function log() { | |
31 | |
32 var output = []; | |
33 for (var i = 0; i < arguments.length; i++) { | |
34 if (typeof arguments[i] === "function") { | |
35 arguments[i] = '' + arguments[i]; | |
36 } | |
37 if (typeof arguments[i] === "string") { | |
38 var bits = arguments[i].replace(/\s*$/, '').split('\n'); | |
39 if (bits.length > 5) { | |
40 bits.splice(3, bits.length-5, '...'); | |
41 } | |
42 output.push(bits.join('\n')); | |
43 } else if (typeof arguments[i] === "object" && typeof arguments[i].name ===
"string") { | |
44 output.push('"'+arguments[i].name+'"'); | |
45 } else { | |
46 output.push(JSON.stringify(arguments[i], undefined, 2)); | |
47 } | |
48 output.push(' '); | |
49 } | |
50 log_element.appendChild(document.createTextNode(output.join('') + '\n')); | |
51 } | |
52 | |
53 var thisScript = document.querySelector("script[src$='bootstrap.js']"); | |
54 var coverageMode = Boolean(parent.window.__coverage__) || /coverage/.test(window
.location.hash); | |
55 | |
56 // Inherit these properties from the parent test-runner if any. | |
57 window.__resources__ = parent.window.__resources__ || {original: {}}; | |
58 window.__coverage__ = parent.window.__coverage__; | |
59 | |
60 function getSync(src) { | |
61 var xhr = new XMLHttpRequest(); | |
62 xhr.open('GET', src, false); | |
63 xhr.send(); | |
64 if (xhr.responseCode > 400) { | |
65 console.error('Error loading ' + src); | |
66 return ''; | |
67 } | |
68 return xhr.responseText; | |
69 } | |
70 | |
71 function loadScript(src, options) { | |
72 // Add changing parameter to prevent script caching. | |
73 options = options || {coverage: true}; | |
74 if (window.__resources__[src]) { | |
75 document.write('<script type="text/javascript">eval(window.__resources__["'+
src+'"]);</script>'); | |
76 } else if (coverageMode && options.coverage) { | |
77 instrument(src); | |
78 loadScript(src); | |
79 } else { | |
80 if (!inExploreMode()) { | |
81 src += '?' + getCacheBuster(); | |
82 } | |
83 document.write('<script type="text/javascript" src="'+ src + '"></script>'); | |
84 } | |
85 } | |
86 | |
87 function loadCSS(src) { | |
88 document.write('<link rel="stylesheet" type="text/css" href="' + src + '">'); | |
89 } | |
90 | |
91 function forEach(array, callback, thisObj) { | |
92 for (var i=0; i < array.length; i++) { | |
93 if (array.hasOwnProperty(i)) { | |
94 callback.call(thisObj, array[i], i, array); | |
95 } | |
96 } | |
97 } | |
98 | |
99 function hasFlag(flag) { | |
100 return thisScript && thisScript.getAttribute(flag) !== null; | |
101 } | |
102 | |
103 function testType() { | |
104 var p = location.pathname; | |
105 p = p.replace(/^disabled-/, ''); | |
106 | |
107 var match = /(auto|impl|manual|unit)-test[^\\\/]*$/.exec(p); | |
108 return match ? match[1]: 'unknown'; | |
109 } | |
110 | |
111 function inExploreMode() { | |
112 return '#explore' == window.location.hash || window.location.hash.length == 0; | |
113 } | |
114 | |
115 /** | |
116 * Get a value for busting the cache. If we got given a cache buster, pass it | |
117 * along, otherwise generate a new one. | |
118 */ | |
119 var cacheBusterValue = '' + window.Date.now(); | |
120 function getCacheBuster() { | |
121 if (window.location.search.length > 0) | |
122 cacheBusterValue = window.location.search.substr(1, window.location.search.l
ength); | |
123 return cacheBusterValue; | |
124 } | |
125 | |
126 var instrumentationDepsLoaded = false; | |
127 /** | |
128 * Instrument the source at {@code location} and store it in | |
129 * {@code window.__resources__[name]}. | |
130 */ | |
131 function instrument(src) { | |
132 if (__resources__[src]) { | |
133 return; | |
134 } | |
135 if (!instrumentationDepsLoaded) { | |
136 instrumentationDepsLoaded = true; | |
137 (function() { | |
138 eval(getSync('../coverage/esprima/esprima.js')); | |
139 eval(getSync('../coverage/escodegen/escodegen.browser.js')); | |
140 eval(getSync('../coverage/istanbul/lib/instrumenter.js')); | |
141 }).call(window); | |
142 } | |
143 var js = getSync(src); | |
144 window.__resources__.original[src] = js; | |
145 var inst = window.__resources__[src] = new Instrumenter().instrumentSync(js, s
rc); | |
146 } | |
147 | |
148 var svg_namespace_uri = 'http://www.w3.org/2000/svg'; | |
149 | |
150 /** | |
151 * Figure out a useful name for an element. | |
152 * | |
153 * @param {Element} element Element to get the name for. | |
154 * | |
155 * @private | |
156 */ | |
157 function _element_name(element) { | |
158 if (element.id) { | |
159 return element.tagName.toLowerCase() + '#' + element.id; | |
160 } else { | |
161 return 'An anonymous ' + element.tagName.toLowerCase(); | |
162 } | |
163 } | |
164 | |
165 /** | |
166 * Get the style for a given element. | |
167 * | |
168 * @param {Array.<Object.<string, string>>|Object.<string, string>} style | |
169 * Either; | |
170 * * A list of dictionaries, each node returned is checked against the | |
171 * associated dictionary, or | |
172 * * A single dictionary, each node returned is checked against the | |
173 * given dictionary. | |
174 * Each dictionary should be of the form {style_name: style_value}. | |
175 * | |
176 * @private | |
177 */ | |
178 function _assert_style_get(style, i) { | |
179 if (typeof style[i] === 'undefined') { | |
180 return style; | |
181 } else { | |
182 return style[i]; | |
183 } | |
184 } | |
185 | |
186 /** | |
187 * Extract all the informative parts of a string. Ignores spacing, punctuation | |
188 * and other random extra characters. | |
189 */ | |
190 function _extract_important(input) { | |
191 var re = /([-+]?[0-9]+\.?[0-9]*(?:[eE][-+]?[0-9]+)?)|[A-Za-z%]+/g; | |
192 | |
193 var match; | |
194 var result = []; | |
195 while (match = re.exec(input)) { | |
196 var value = match[0]; | |
197 if (typeof match[1] != "undefined") { | |
198 value = Number(match[1]); | |
199 } | |
200 result.push(value); | |
201 } | |
202 return result; | |
203 } | |
204 window.assert_styles_extract_important = _extract_important; | |
205 | |
206 function AssertionError(message) { | |
207 this.message = message; | |
208 } | |
209 window.assert_styles_assertion_error = AssertionError; | |
210 | |
211 /** | |
212 * Asserts that a string is in the array of expected only comparing the | |
213 * important parts. Ignores spacing, punctuation and other random extra | |
214 * characters. | |
215 */ | |
216 function _assert_important_in_array(actual, expected, message) { | |
217 var actual_array = _extract_important(actual); | |
218 | |
219 var expected_array_array = []; | |
220 for (var i = 0; i < expected.length; i++) { | |
221 expected_array_array.push(_extract_important(expected[i])); | |
222 } | |
223 | |
224 var errors = []; | |
225 for (var i = 0; i < expected_array_array.length; i++) { | |
226 var expected_array = expected_array_array[i]; | |
227 | |
228 var element_errors = []; | |
229 if (actual_array.length != expected_array.length) { | |
230 element_errors.push('Number of elements don\'t match'); | |
231 } | |
232 | |
233 for (var j = 0; j < expected_array.length; j++) { | |
234 var actual = actual_array[j]; | |
235 var expected = expected_array[j]; | |
236 | |
237 try { | |
238 assert_equals(typeof actual, typeof expected); | |
239 | |
240 if (typeof actual === 'number') { | |
241 if (Math.abs(actual) < 1e-10) { | |
242 actual = 0; | |
243 } | |
244 actual = '' + actual.toPrecision(4); | |
245 } | |
246 if (typeof expected === 'number') { | |
247 if (Math.abs(expected) < 1e-10) { | |
248 expected = 0; | |
249 } | |
250 expected = '' + expected.toPrecision(4); | |
251 } | |
252 | |
253 assert_equals(actual, expected); | |
254 } catch (e) { | |
255 element_errors.push( | |
256 'Element ' + j + ' - ' + e.message); | |
257 } | |
258 } | |
259 | |
260 if (element_errors.length == 0) { | |
261 return; | |
262 } else { | |
263 errors.push( | |
264 ' Expectation ' + JSON.stringify(expected_array) + ' did not match\n'
+ | |
265 ' ' + element_errors.join('\n ')); | |
266 } | |
267 } | |
268 if (expected_array_array.length > 1) | |
269 errors.unshift(' ' + expected_array_array.length + ' possible expectations'
); | |
270 | |
271 errors.unshift(' Actual - ' + JSON.stringify(actual_array)); | |
272 if (typeof message !== 'undefined') { | |
273 errors.unshift(message); | |
274 } | |
275 throw new AssertionError(errors.join('\n')); | |
276 } | |
277 window.assert_styles_assert_important_in_array = _assert_important_in_array; | |
278 | |
279 /** | |
280 * asserts that actual has the same styles as the dictionary given by | |
281 * expected. | |
282 * | |
283 * @param {Element} object DOM node to check the styles on | |
284 * @param {Object.<string, string>} styles Dictionary of {style_name: style_valu
e} to check | |
285 * on the object. | |
286 * @param {String} description Human readable description of what you are | |
287 * trying to check. | |
288 * | |
289 * @private | |
290 */ | |
291 function _assert_style_element(object, style, description) { | |
292 if (typeof message == 'undefined') | |
293 description = ''; | |
294 | |
295 // Create an element of the same type as testing so the style can be applied | |
296 // from the test. This is so the css property (not the -webkit-does-something | |
297 // tag) can be read. | |
298 var reference_element = (object.namespaceURI == svg_namespace_uri) ? | |
299 document.createElementNS(svg_namespace_uri, object.nodeName) : | |
300 document.createElement(object.nodeName); | |
301 var computedObjectStyle = getComputedStyle(object, null); | |
302 for (var i = 0; i < computedObjectStyle.length; i++) { | |
303 var property = computedObjectStyle[i]; | |
304 reference_element.style.setProperty(property, | |
305 computedObjectStyle.getPropertyValue(property)); | |
306 } | |
307 reference_element.style.position = 'absolute'; | |
308 if (object.parentNode) { | |
309 object.parentNode.appendChild(reference_element); | |
310 } | |
311 | |
312 try { | |
313 // Apply the style | |
314 for (var prop_name in style) { | |
315 // If the passed in value is an element then grab its current style for | |
316 // that property | |
317 if (style[prop_name] instanceof HTMLElement || | |
318 style[prop_name] instanceof SVGElement) { | |
319 | |
320 var prop_value = getComputedStyle(style[prop_name], null)[prop_name]; | |
321 } else { | |
322 var prop_value = style[prop_name]; | |
323 } | |
324 | |
325 prop_value = '' + prop_value; | |
326 | |
327 var output_prop_name = _WebAnimationsTestingUtilities._prefixProperty(prop
_name); | |
328 | |
329 var is_svg = _WebAnimationsTestingUtilities._propertyIsSVGAttrib(prop_name
, object); | |
330 if (is_svg) { | |
331 reference_element.setAttribute(prop_name, prop_value); | |
332 | |
333 var current_style = object.attributes; | |
334 var target_style = reference_element.attributes; | |
335 } else { | |
336 reference_element.style[output_prop_name] = prop_value; | |
337 | |
338 var current_style = computedObjectStyle; | |
339 var target_style = getComputedStyle(reference_element, null); | |
340 | |
341 _assert_important_in_array( | |
342 prop_value, [reference_element.style[output_prop_name], target_style
[output_prop_name]], | |
343 'Tried to set the reference element\'s '+ output_prop_name + | |
344 ' to ' + JSON.stringify(prop_value) + | |
345 ' but neither the style' + | |
346 ' ' + JSON.stringify(reference_element.style[output_prop_name]) + | |
347 ' nor computedStyle ' + JSON.stringify(target) + | |
348 ' ended up matching requested value.'); | |
349 } | |
350 | |
351 if (prop_name == 'ctm') { | |
352 var ctm = object.getCTM(); | |
353 var curr = '{' + ctm.a + ', ' + | |
354 ctm.b + ', ' + ctm.c + ', ' + ctm.d + ', ' + | |
355 ctm.e + ', ' + ctm.f + '}'; | |
356 | |
357 var target = prop_value; | |
358 | |
359 } else if (is_svg) { | |
360 var target = target_style[prop_name].value; | |
361 var curr = current_style[prop_name].value; | |
362 } else { | |
363 var target = target_style[output_prop_name]; | |
364 var curr = current_style[output_prop_name]; | |
365 } | |
366 | |
367 var description_extra = '\n Property ' + prop_name; | |
368 if (prop_name != output_prop_name) | |
369 description_extra += '(actually ' + output_prop_name + ')'; | |
370 | |
371 _assert_important_in_array(curr, [target], description + description_extra
); | |
372 } | |
373 } finally { | |
374 if (reference_element.parentNode) { | |
375 reference_element.parentNode.removeChild(reference_element); | |
376 } | |
377 } | |
378 } | |
379 | |
380 /** | |
381 * asserts that elements in the list have given styles. | |
382 * | |
383 * @param {Array.<Element>} objects List of DOM nodes to check the styles on | |
384 * @param {Array.<Object.<string, string>>|Object.<string, string>} style | |
385 * See _assert_style_get for information. | |
386 * @param {String} description Human readable description of what you are | |
387 * trying to check. | |
388 * | |
389 * @private | |
390 */ | |
391 function _assert_style_element_list(objects, style, description) { | |
392 var error = ''; | |
393 forEach(objects, function(object, i) { | |
394 try { | |
395 _assert_style_element( | |
396 object, _assert_style_get(style, i), | |
397 description + ' ' + _element_name(object) | |
398 ); | |
399 } catch (e) { | |
400 if (error) { | |
401 error += '; '; | |
402 } | |
403 error += 'Element ' + _element_name(object) + ' at index ' + i + ' failed
' + e.message + '\n'; | |
404 } | |
405 }); | |
406 if (error) { | |
407 throw error; | |
408 } | |
409 } | |
410 | |
411 /** | |
412 * asserts that elements returned from a query selector have a list of styles. | |
413 * | |
414 * @param {string} qs A query selector to use to get the DOM nodes. | |
415 * @param {Array.<Object.<string, string>>|Object.<string, string>} style | |
416 * See _assert_style_get for information. | |
417 * @param {String} description Human readable description of what you are | |
418 * trying to check. | |
419 * | |
420 * @private | |
421 */ | |
422 function _assert_style_queryselector(qs, style, description) { | |
423 var objects = document.querySelectorAll(qs); | |
424 assert_true(objects.length > 0, description + | |
425 ' is invalid, no elements match query selector: ' + qs); | |
426 _assert_style_element_list(objects, style, description); | |
427 } | |
428 | |
429 /** | |
430 * asserts that elements returned from a query selector have a list of styles. | |
431 * | |
432 * Assert the element with id #hello is 100px wide; | |
433 * assert_styles(document.getElementById('hello'), {'width': '100px'}) | |
434 * assert_styles('#hello'), {'width': '100px'}) | |
435 * | |
436 * Assert all divs are 100px wide; | |
437 * assert_styles(document.getElementsByTagName('div'), {'width': '100px'}) | |
438 * assert_styles('div', {'width': '100px'}) | |
439 * | |
440 * Assert all objects with class 'red' are 100px wide; | |
441 * assert_styles(document.getElementsByClassName('red'), {'width': '100px'}) | |
442 * assert_styles('.red', {'width': '100px'}) | |
443 * | |
444 * Assert first div is 100px wide, second div is 200px wide; | |
445 * assert_styles(document.getElementsByTagName('div'), | |
446 * [{'width': '100px'}, {'width': '200px'}]) | |
447 * assert_styles('div', | |
448 * [{'width': '100px'}, {'width': '200px'}]) | |
449 * | |
450 * @param {string|Element|Array.<Element>} objects Either; | |
451 * * A query selector to use to get DOM nodes, | |
452 * * A DOM node. | |
453 * * A list of DOM nodes. | |
454 * @param {Array.<Object.<string, string>>|Object.<string, string>} style | |
455 * See _assert_style_get for information. | |
456 */ | |
457 function assert_styles(objects, style, description) { | |
458 switch (typeof objects) { | |
459 case 'string': | |
460 _assert_style_queryselector(objects, style, description); | |
461 break; | |
462 | |
463 case 'object': | |
464 if (objects instanceof Array || objects instanceof NodeList) { | |
465 _assert_style_element_list(objects, style, description); | |
466 } else if (objects instanceof Element) { | |
467 _assert_style_element(objects, style, description); | |
468 } else { | |
469 throw new Error('Expected Array, NodeList or Element but got ' + objects
); | |
470 } | |
471 break; | |
472 } | |
473 } | |
474 window.assert_styles = assert_styles; | |
475 | |
476 /** | |
477 * Schedule something to be called at a given time. | |
478 * | |
479 * @constructor | |
480 * @param {number} millis Milliseconds after start at which the callback should | |
481 * be called. | |
482 * @param {bool} autostart Auto something... | |
483 */ | |
484 function TestTimelineGroup(millis) { | |
485 this.millis = millis; | |
486 | |
487 /** | |
488 * @type {bool} | |
489 */ | |
490 this.autorun_ = false; | |
491 | |
492 /** | |
493 * @type {!Array.<function(): ?Object>} | |
494 */ | |
495 this.startCallbacks = null; | |
496 | |
497 /** | |
498 * Callbacks which are added after the timeline has started. We clear them | |
499 * when going backwards. | |
500 * | |
501 * @type {?Array.<function(): ?Object>} | |
502 */ | |
503 this.lateCallbacks = null; | |
504 | |
505 /** | |
506 * @type {Element} | |
507 */ | |
508 this.marker = document.createElement('img'); | |
509 /** | |
510 * @type {Element} | |
511 */ | |
512 this.info = document.createElement('div'); | |
513 | |
514 this.setup_(); | |
515 } | |
516 | |
517 TestTimelineGroup.prototype.setup_ = function() { | |
518 this.endTime_ = 0; | |
519 this.startCallbacks = new Array(); | |
520 this.lateCallbacks = null; | |
521 this.marker.innerHTML = ''; | |
522 this.info.innerHTML = ''; | |
523 }; | |
524 | |
525 /** | |
526 * Add a new callback to the event group | |
527 * | |
528 * @param {function(): ?Object} callback Callback given the currentTime of | |
529 * callback. | |
530 */ | |
531 TestTimelineGroup.prototype.add = function(callback) { | |
532 if (this.lateCallbacks === null) { | |
533 this.startCallbacks.unshift(callback); | |
534 } else { | |
535 this.lateCallbacks.unshift(callback); | |
536 } | |
537 | |
538 // Trim out extra 'function() { ... }' | |
539 var callbackString = callback.name; | |
540 // FIXME: This should probably unindent too.... | |
541 this.info.innerHTML += '<div>' + callbackString + '</div>'; | |
542 }; | |
543 | |
544 /** | |
545 * Reset this event group to the state before start was called. | |
546 */ | |
547 TestTimelineGroup.prototype.reset = function() { | |
548 this.lateCallbacks = null; | |
549 | |
550 var callbacks = this.startCallbacks.slice(0); | |
551 this.setup_(); | |
552 while (callbacks.length > 0) { | |
553 var callback = callbacks.shift(); | |
554 this.add(callback); | |
555 } | |
556 }; | |
557 | |
558 /** | |
559 * Tell the event group that the timeline has started and that any callbacks | |
560 * added from now are dynamically generated and hence should be cleared when a | |
561 * reset is called. | |
562 */ | |
563 TestTimelineGroup.prototype.start = function() { | |
564 this.lateCallbacks = new Array(); | |
565 }; | |
566 | |
567 /** | |
568 * Call all the callbacks in the EventGroup. | |
569 */ | |
570 TestTimelineGroup.prototype.call = function() { | |
571 var callbacks = (this.startCallbacks.slice(0)).concat(this.lateCallbacks); | |
572 var statuses = this.info.children; | |
573 | |
574 var overallResult = true; | |
575 while (callbacks.length > 0) { | |
576 var callback = callbacks.pop(); | |
577 | |
578 var status_ = statuses[statuses.length - callbacks.length - 1]; | |
579 | |
580 if (typeof callback == 'function') { | |
581 log('TestTimelineGroup', 'calling function', callback); | |
582 try { | |
583 callback(); | |
584 } catch (e) { | |
585 // On IE the only way to get the real stack is to do this | |
586 window.onerror(e.message, e.fileName, e.lineNumber, e); | |
587 // On other browsers we want to throw the error later | |
588 setTimeout(function () { throw e; }, 0); | |
589 } | |
590 } else { | |
591 log('TestTimelineGroup', 'calling test', callback); | |
592 var result = callback.step(callback.f); | |
593 callback.done(); | |
594 } | |
595 | |
596 if (result === undefined || result == null) { | |
597 overallResult = overallResult && true; | |
598 | |
599 status_.style.color = 'green'; | |
600 } else { | |
601 overallResult = overallResult && false; | |
602 status_.style.color = 'red'; | |
603 status_.innerHTML += '<div>' + result.toString() + '</div>'; | |
604 } | |
605 } | |
606 if (overallResult) { | |
607 this.marker.src = '../img/success.png'; | |
608 } else { | |
609 this.marker.src = '../img/error.png'; | |
610 } | |
611 } | |
612 | |
613 /** | |
614 * Draw the EventGroup's marker at the correct position on the timeline. | |
615 * | |
616 * FIXME(mithro): This mixes display and control :( | |
617 * | |
618 * @param {number} endTime The endtime of the timeline in millis. Used to | |
619 * display the marker at the right place on the timeline. | |
620 */ | |
621 TestTimelineGroup.prototype.draw = function(container, endTime) { | |
622 this.marker.title = this.millis + 'ms'; | |
623 this.marker.className = 'marker'; | |
624 this.marker.src = '../img/unknown.png'; | |
625 | |
626 var mleft = 'calc(100% - 10px)'; | |
627 if (endTime != 0) { | |
628 mleft = 'calc(' + (this.millis / endTime) * 100.0 + '%' + ' - 10px)'; | |
629 } | |
630 this.marker.style.left = mleft; | |
631 | |
632 container.appendChild(this.marker); | |
633 | |
634 this.info.className = 'info'; | |
635 container.appendChild(this.info); | |
636 | |
637 // Display details about the events at this time period when hovering over | |
638 // the marker. | |
639 this.marker.onmouseover = function() { | |
640 this.style.display = 'block'; | |
641 }.bind(this.info); | |
642 | |
643 this.marker.onmouseout = function() { | |
644 this.style.display = 'none'; | |
645 }.bind(this.info); | |
646 | |
647 | |
648 var offset = Math.ceil(this.info.offsetWidth / 2); | |
649 var ileft = 'calc(100% - ' + offset + 'px)'; | |
650 if (endTime != 0) { | |
651 ileft = 'calc(' + (this.millis / endTime) * 100.0 + '%' + ' - ' + offset + | |
652 'px)'; | |
653 } | |
654 this.info.style.left = ileft; | |
655 | |
656 this.info.style.display = 'none'; | |
657 }; | |
658 | |
659 | |
660 | |
661 /** | |
662 * Moves the testharness_timeline in "real time". | |
663 * (IE 1 test second takes 1 real second). | |
664 * | |
665 * @constructor | |
666 */ | |
667 function RealtimeRunner(timeline) { | |
668 this.timeline = timeline; | |
669 | |
670 // Capture the real requestAnimationFrame so we can run in 'real time' mode | |
671 // rather than as fast as possible. | |
672 var nativeRequestAnimationFrame = window.requestAnimationFrame || window.mozRe
questAnimationFrame; | |
673 this.boundRequestAnimationFrame = function(f) { | |
674 nativeRequestAnimationFrame(f.bind(this)) | |
675 }; | |
676 this.now = window.Date.now; | |
677 | |
678 this.zeroTime = null; // Time the page loaded | |
679 this.pauseStartTime = null; // Time at which we paused raf | |
680 this.timeDrift = 0; // Amount we have been stopped for | |
681 } | |
682 | |
683 /** | |
684 * Callback called from nativeRequestAnimationFrame. | |
685 * | |
686 * @private | |
687 * @param {number} timestamp The current time for the animation frame | |
688 * (in millis). | |
689 */ | |
690 RealtimeRunner.prototype.animationFrame_ = function(timestamp) { | |
691 if (this.zeroTime === null) { | |
692 this.zeroTime = timestamp; | |
693 } | |
694 | |
695 // Are we paused? Stop calling requestAnimationFrame. | |
696 if (this.pauseStartTime != null) { | |
697 return; | |
698 } | |
699 | |
700 var virtualAnimationTime = timestamp - this.zeroTime - this.timeDrift; | |
701 var endTime = this.timeline.endTime_; | |
702 // If we have no events paste t=0, endTime is going to be zero. Instead | |
703 // make the test run for 2 minutes. | |
704 if (endTime == 0) { | |
705 endTime = 120e3; | |
706 } | |
707 | |
708 // Do we still have time to go? | |
709 if (virtualAnimationTime < endTime) { | |
710 try { | |
711 this.timeline.setTime(virtualAnimationTime); | |
712 } finally { | |
713 this.boundRequestAnimationFrame(this.animationFrame_); | |
714 } | |
715 | |
716 } else { | |
717 // Have we gone past endTime_? Force the harness to its endTime_. | |
718 | |
719 this.timeline.setTime(endTime); | |
720 // Don't continue to raf | |
721 } | |
722 }; | |
723 | |
724 RealtimeRunner.prototype.start = function() { | |
725 if (this.pauseStartTime != null) { | |
726 this.timeDrift += (this.now() - this.pauseStartTime); | |
727 this.pauseStartTime = null; | |
728 } | |
729 this.boundRequestAnimationFrame(this.animationFrame_); | |
730 }; | |
731 | |
732 RealtimeRunner.prototype.pause = function() { | |
733 if (this.pauseStartTime != null) { | |
734 return; | |
735 } | |
736 this.pauseStartTime = this.now(); | |
737 }; | |
738 | |
739 | |
740 /** | |
741 * Class for storing events that happen during at given times (such as | |
742 * animation checks, or setTimeout). | |
743 * | |
744 * @constructor | |
745 */ | |
746 function TestTimeline(everyFrame) { | |
747 log('TestTimeline', 'constructor', everyFrame); | |
748 /** | |
749 * Stores the events which are upcoming. | |
750 * | |
751 * @type Object.<number, TestTimelineGroup> | |
752 * @private | |
753 */ | |
754 this.timeline_ = new Array(); | |
755 | |
756 this.everyFrame = everyFrame; | |
757 this.frameMillis = 1000.0 / 60; //60fps | |
758 | |
759 this.currentTime_ = -this.frameMillis; | |
760 | |
761 // Schedule an event at t=0, needed temporarily. | |
762 this.schedule(function() {}, 0); | |
763 | |
764 this.reset(); | |
765 | |
766 this.runner_ = new RealtimeRunner(this); | |
767 } | |
768 | |
769 /** | |
770 * Create the GUI controller for the timeline. | |
771 * @param {Element} body DOM element to add the GUI too, normally the <body> | |
772 * element. | |
773 */ | |
774 TestTimeline.prototype.createGUI = function(body) { | |
775 // HTML needed to create the timeline UI | |
776 this.div = document.createElement('div'); | |
777 this.div.id = 'timeline'; | |
778 | |
779 this.timelinebar = document.createElement('div'); | |
780 this.timelinebar.className = 'bar'; | |
781 | |
782 this.timelineprogress = document.createElement('div'); | |
783 this.timelineprogress.className = 'progress'; | |
784 | |
785 this.timelinebar.appendChild(this.timelineprogress); | |
786 this.div.appendChild(this.timelinebar); | |
787 | |
788 this.next = document.createElement('button'); | |
789 this.next.innerText = '>'; | |
790 this.next.id = 'next'; | |
791 this.next.onclick = this.toNextEvent.bind(this); | |
792 this.div.appendChild(this.next); | |
793 | |
794 this.prev = document.createElement('button'); | |
795 this.prev.innerText = '<'; | |
796 this.prev.id = 'prev'; | |
797 this.prev.onclick = this.toPrevEvent.bind(this); | |
798 this.div.appendChild(this.prev); | |
799 | |
800 this.control = document.createElement('button'); | |
801 this.control.innerText = 'Pause'; | |
802 this.control.id = 'control'; | |
803 this.control.onclick = function() { | |
804 if (this.control.innerText == 'Go!') { | |
805 this.runner_.start(); | |
806 this.control.innerText = 'Pause'; | |
807 } else { | |
808 this.runner_.pause(); | |
809 this.control.innerText = 'Go!'; | |
810 } | |
811 }.bind(this); | |
812 this.div.appendChild(this.control); | |
813 | |
814 body.appendChild(this.div); | |
815 } | |
816 | |
817 /** | |
818 * Update GUI elements. | |
819 * | |
820 * @private | |
821 */ | |
822 TestTimeline.prototype.updateGUI = function () { | |
823 // Update the timeline | |
824 var width = "100%"; | |
825 if (this.endTime_ != 0) { | |
826 width = (this.currentTime_ / this.endTime_) * 100.0 +'%' | |
827 } | |
828 this.timelineprogress.style.width = width; | |
829 this.timelinebar.title = (this.currentTime_).toFixed(0) + 'ms'; | |
830 }; | |
831 | |
832 | |
833 /** | |
834 * Sort the timeline into run order. Should be called after adding something to | |
835 * the timeline. | |
836 * | |
837 * @private | |
838 */ | |
839 TestTimeline.prototype.sort_ = function() { | |
840 this.timeline_.sort(function(a,b) { | |
841 return a.millis - b.millis; | |
842 }); | |
843 }; | |
844 | |
845 /** | |
846 * Schedule something to be called at a given time. | |
847 * | |
848 * @param {function(number)} callback Callback to call after the number of milli
s | |
849 * have elapsed. | |
850 * @param {number} millis Milliseconds after start at which the callback should | |
851 * be called. | |
852 */ | |
853 TestTimeline.prototype.schedule = function(callback, millis) { | |
854 log('TestTimeline', 'schedule', millis, callback); | |
855 if (millis < this.currentTime_) { | |
856 // Can't schedule something in the past? | |
857 return; | |
858 } | |
859 | |
860 // See if there is something at that time in the timeline already? | |
861 var timeline = this.timeline_.slice(0); | |
862 var group = null; | |
863 while (timeline.length > 0) { | |
864 if (timeline[0].millis == millis) { | |
865 group = timeline[0]; | |
866 break; | |
867 } else { | |
868 timeline.shift(); | |
869 } | |
870 } | |
871 | |
872 // If not, create a node at that time. | |
873 if (group === null) { | |
874 group = new TestTimelineGroup(millis); | |
875 this.timeline_.unshift(group); | |
876 this.sort_(); | |
877 } | |
878 group.add(callback); | |
879 | |
880 var newEndTime = this.timeline_.slice(-1)[0].millis * 1.1; | |
881 if (this.endTime_ != newEndTime) { | |
882 this.endTime_ = newEndTime; | |
883 } | |
884 }; | |
885 | |
886 /** | |
887 * Return the current time in milliseconds. | |
888 */ | |
889 TestTimeline.prototype.now = function() { | |
890 log('TestTimeline', 'now', Math.max(this.currentTime_, 0)); | |
891 return Math.max(this.currentTime_, 0); | |
892 }; | |
893 | |
894 /** | |
895 * Set the current time to a given value. | |
896 * | |
897 * @param {number} millis Time in milliseconds to set the current time too. | |
898 */ | |
899 TestTimeline.prototype.setTime = function(millis) { | |
900 log('TestTimeline', 'setTime', millis); | |
901 // Time is going backwards, we actually have to reset and go forwards as | |
902 // events can cause the creation of more events. | |
903 if (this.currentTime_ > millis) { | |
904 this.reset(); | |
905 this.start(); | |
906 } | |
907 | |
908 var events = this.timeline_.slice(0); | |
909 | |
910 // Already processed events | |
911 while (events.length > 0 && events[0].millis <= this.currentTime_) { | |
912 events.shift(); | |
913 } | |
914 | |
915 while (this.currentTime_ < millis) { | |
916 var event_ = null; | |
917 var moveTo = millis; | |
918 | |
919 if (events.length > 0 && events[0].millis <= millis) { | |
920 event_ = events.shift(); | |
921 moveTo = event_.millis; | |
922 } | |
923 | |
924 // Call the callback | |
925 if (this.currentTime_ != moveTo) { | |
926 log('TestTimeline', 'setting time to', moveTo); | |
927 this.currentTime_ = moveTo; | |
928 this.animationFrame(this.currentTime_); | |
929 } | |
930 | |
931 if (event_) { | |
932 event_.call(); | |
933 } | |
934 } | |
935 | |
936 this.updateGUI(); | |
937 | |
938 if (millis >= this.endTime_) { | |
939 this.done(); | |
940 } | |
941 }; | |
942 | |
943 /** | |
944 * Call all callbacks registered for the next (virtual) animation frame. | |
945 * | |
946 * @param {number} millis Time in milliseconds. | |
947 * @private | |
948 */ | |
949 TestTimeline.prototype.animationFrame = function(millis) { | |
950 /* FIXME(mithro): Code should appear here to allow testing of running | |
951 * every animation frame. | |
952 | |
953 if (this.everyFrame) { | |
954 } | |
955 | |
956 */ | |
957 | |
958 var callbacks = this.animationFrameCallbacks; | |
959 callbacks.reverse(); | |
960 this.animationFrameCallbacks = []; | |
961 for (var i = 0; i < callbacks.length; i++) { | |
962 log('TestTimeline raf callback', callbacks[i], millis); | |
963 try { | |
964 callbacks[i](millis); | |
965 } catch (e) { | |
966 // On IE the only way to get the real stack is to do this | |
967 window.onerror(e.message, e.fileName, e.lineNumber, e); | |
968 // On other browsers we want to throw the error later | |
969 setTimeout(function () { throw e; }, 0); | |
970 } | |
971 } | |
972 }; | |
973 | |
974 /** | |
975 * Set a callback to run at the next (virtual) animation frame. | |
976 * | |
977 * @param {function(millis)} millis Time in milliseconds to set the current | |
978 * time too. | |
979 */ | |
980 TestTimeline.prototype.requestAnimationFrame = function(callback) { | |
981 // FIXME: This should return a reference that allows people to cancel the | |
982 // animationFrame callback. | |
983 this.animationFrameCallbacks.push(callback); | |
984 return -1; | |
985 }; | |
986 | |
987 /** | |
988 * Go to next scheduled event in timeline. | |
989 */ | |
990 TestTimeline.prototype.toNextEvent = function() { | |
991 var events = this.timeline_.slice(0); | |
992 while (events.length > 0 && events[0].millis <= this.currentTime_) { | |
993 events.shift(); | |
994 } | |
995 if (events.length > 0) { | |
996 this.setTime(events[0].millis); | |
997 | |
998 if (this.autorun_) { | |
999 setTimeout(this.toNextEvent.bind(this), 0); | |
1000 } | |
1001 | |
1002 return true; | |
1003 } else { | |
1004 this.setTime(this.endTime_); | |
1005 return false; | |
1006 } | |
1007 | |
1008 }; | |
1009 | |
1010 /** | |
1011 * Go to previous scheduled event in timeline. | |
1012 * (This actually goes back to time zero and then forward to this event.) | |
1013 */ | |
1014 TestTimeline.prototype.toPrevEvent = function() { | |
1015 var events = this.timeline_.slice(0); | |
1016 while (events.length > 0 && | |
1017 events[events.length - 1].millis >= this.currentTime_) { | |
1018 events.pop(); | |
1019 } | |
1020 if (events.length > 0) { | |
1021 this.setTime(events[events.length - 1].millis); | |
1022 return true; | |
1023 } else { | |
1024 this.setTime(0); | |
1025 return false; | |
1026 } | |
1027 }; | |
1028 | |
1029 /** | |
1030 * Reset the timeline to time zero. | |
1031 */ | |
1032 TestTimeline.prototype.reset = function () { | |
1033 for (var t in this.timeline_) { | |
1034 this.timeline_[t].reset(); | |
1035 } | |
1036 | |
1037 this.currentTime_ = -this.frameMillis; | |
1038 this.animationFrameCallbacks = []; | |
1039 this.started_ = false; | |
1040 }; | |
1041 | |
1042 /** | |
1043 * Call to initiate starting??? | |
1044 */ | |
1045 TestTimeline.prototype.start = function () { | |
1046 this.started_ = true; | |
1047 | |
1048 var parent = this; | |
1049 | |
1050 for (var t in this.timeline_) { | |
1051 this.timeline_[t].start(); | |
1052 // FIXME(mithro) this is confusing... | |
1053 this.timeline_[t].draw(this.timelinebar, this.endTime_); | |
1054 | |
1055 this.timeline_[t].marker.onclick = function(event) { | |
1056 parent.setTime(this.millis); | |
1057 event.stopPropagation(); | |
1058 }.bind(this.timeline_[t]); | |
1059 } | |
1060 | |
1061 this.timelinebar.onclick = function(evt) { | |
1062 var setPercent = | |
1063 ((evt.clientX - this.offsetLeft) / this.offsetWidth); | |
1064 parent.setTime(setPercent * parent.endTime_); | |
1065 }.bind(this.timelinebar); | |
1066 }; | |
1067 | |
1068 TestTimeline.prototype.done = function () { | |
1069 log('TestTime', 'done'); | |
1070 done(); | |
1071 }; | |
1072 | |
1073 TestTimeline.prototype.autorun = function() { | |
1074 this.autorun_ = true; | |
1075 this.toNextEvent(); | |
1076 }; | |
1077 | |
1078 function testharness_timeline_setup() { | |
1079 log('testharness_timeline_setup'); | |
1080 testharness_timeline.createGUI(document.getElementsByTagName('body')[0]); | |
1081 testharness_timeline.start(); | |
1082 testharness_timeline.updateGUI(); | |
1083 | |
1084 // Start running the test on message | |
1085 if ('#message' == window.location.hash) { | |
1086 window.addEventListener('message', function(evt) { | |
1087 switch (evt.data['type']) { | |
1088 case 'start': | |
1089 if (evt.data['url'] == window.location.href) { | |
1090 testharness_timeline.autorun(); | |
1091 } | |
1092 break; | |
1093 } | |
1094 }); | |
1095 } else if ('#auto' == window.location.hash || '#coverage' == window.location.h
ash) { | |
1096 // Run the test as fast as possible, skipping time. | |
1097 | |
1098 // Need non-zero timeout to allow chrome to run other code. | |
1099 setTimeout(testharness_timeline.autorun.bind(testharness_timeline), 1); | |
1100 | |
1101 } else if (inExploreMode()) { | |
1102 setTimeout(testharness_timeline.runner_.start.bind(testharness_timeline.runn
er_), 1); | |
1103 } else { | |
1104 alert('Unknown start mode.'); | |
1105 } | |
1106 } | |
1107 | |
1108 // Capture testharness's test as we are about to screw with it. | |
1109 var testharness_test = window.test; | |
1110 | |
1111 function override_at(replacement_at, f, args) { | |
1112 var orig_at = window.at; | |
1113 window.at = replacement_at; | |
1114 f.apply(null, args); | |
1115 window.at = orig_at; | |
1116 } | |
1117 | |
1118 function timing_test(f, desc) { | |
1119 /** | |
1120 * at function inside a timing_test function allows testing things at a | |
1121 * given time rather then onload. | |
1122 * @param {number} millis Milliseconds after page load to run the tests. | |
1123 * @param {function()} f Closure containing the asserts to be run. | |
1124 * @param {string} desc Description | |
1125 */ | |
1126 var at = function(millis, f, desc_at) { | |
1127 assert_true(typeof millis == 'number', "at's first argument shoud be a numbe
r."); | |
1128 assert_true(!isNaN(millis), "at's first argument should be a number not NaN!
"); | |
1129 assert_true(millis >= 0, "at's first argument should be greater then 0."); | |
1130 assert_true(isFinite(millis), "at's first argument should be finite."); | |
1131 | |
1132 assert_true(typeof f == 'function', "at's second argument should be a functi
on."); | |
1133 | |
1134 // Deliberately hoist the desc if we where not given one. | |
1135 if (typeof desc_at == 'undefined' || desc_at == null || desc_at.length == 0)
{ | |
1136 desc_at = desc; | |
1137 } | |
1138 | |
1139 // And then provide 'Unnamed' as a default | |
1140 if (typeof desc_at == 'undefined' || desc_at == null || desc_at.length == 0)
{ | |
1141 desc_at = 'Unnamed assert'; | |
1142 } | |
1143 | |
1144 var t = async_test(desc_at + ' at t=' + millis + 'ms'); | |
1145 t.f = f; | |
1146 window.testharness_timeline.schedule(t, millis); | |
1147 }; | |
1148 override_at(at, f); | |
1149 } | |
1150 | |
1151 function test_without_at(f, desc) { | |
1152 // Make sure calling at inside a test() function is a failure. | |
1153 override_at(function() { | |
1154 throw {'message': 'Can not use at() inside a test, use a timing_test instead
.'}; | |
1155 }, function() { testharness_test(f, desc); }); | |
1156 } | |
1157 | |
1158 /** | |
1159 * at function schedules a to be called at a given point. | |
1160 * @param {number} millis Milliseconds after page load to run the function. | |
1161 * @param {function()} f Function to be called. Called with no arguments | |
1162 */ | |
1163 function at(millis, f) { | |
1164 assert_true(typeof millis == 'number', "at's first argument shoud be a number.
"); | |
1165 assert_true(typeof f == 'function', "at's second argument should be a function
."); | |
1166 | |
1167 window.testharness_timeline.schedule(f, millis); | |
1168 } | |
1169 | |
1170 window.testharness_after_loaded = function() { | |
1171 log('testharness_after_loaded'); | |
1172 /** | |
1173 * These steps needs to occur after testharness is loaded. | |
1174 */ | |
1175 setup(function() {}, { | |
1176 explicit_timeout: true, | |
1177 explicit_done: ((typeof window.testharness_timeline) !== 'undefined')}); | |
1178 | |
1179 /** | |
1180 * Create an testharness test which makes sure the page contains no | |
1181 * javascript errors. This is needed otherwise if the page contains errors | |
1182 * then preventing the tests loading it will look like it passed. | |
1183 */ | |
1184 var pageerror_test = async_test('Page contains no errors'); | |
1185 | |
1186 window.onerror = function(msg, url, line, e) { | |
1187 var msg = '\nError in ' + url + '\n' + | |
1188 'Line ' + line + ': ' + msg + '\n'; | |
1189 | |
1190 if (typeof e != "undefined") { | |
1191 msg += e.stack; | |
1192 } | |
1193 | |
1194 pageerror_test.is_done = true; | |
1195 pageerror_test.step(function() { | |
1196 throw new AssertionError(msg); | |
1197 }); | |
1198 pageerror_test.is_done = false; | |
1199 }; | |
1200 | |
1201 var pageerror_tests; | |
1202 function pageerror_othertests_finished(test, harness) { | |
1203 if (harness == null && pageerror_tests == null) { | |
1204 return; | |
1205 } | |
1206 | |
1207 if (pageerror_tests == null) { | |
1208 pageerror_tests = harness; | |
1209 } | |
1210 | |
1211 if (pageerror_tests.all_loaded && pageerror_tests.num_pending == 1) { | |
1212 pageerror_test.done(); | |
1213 } | |
1214 } | |
1215 add_result_callback(pageerror_othertests_finished); | |
1216 addEventListener('load', pageerror_othertests_finished); | |
1217 | |
1218 }; | |
1219 | |
1220 loadScript('../testharness/testharness.js', {coverage: false}); | |
1221 document.write('<script type="text/javascript">window.testharness_after_loaded()
;</script>'); | |
1222 loadCSS('../testharness/testharness.css'); | |
1223 loadCSS('../testharness_timing.css'); | |
1224 | |
1225 if (testType() == 'auto') { | |
1226 var checksFile = location.pathname; | |
1227 checksFile = checksFile.replace(/disabled-/, ''); | |
1228 checksFile = checksFile.replace(/.html$/, '-checks.js') | |
1229 loadScript(checksFile, {coverage: false}); | |
1230 } | |
1231 | |
1232 document.write('<div id="log"></div>'); | |
1233 loadScript('../testharness/testharnessreport.js', {coverage: false}); | |
1234 | |
1235 if (!hasFlag('nopolyfill')) { | |
1236 loadScript('../../web-animations.js'); | |
1237 } | |
1238 | |
1239 addEventListener('load', function() { | |
1240 if (window._WebAnimationsTestingUtilities) { | |
1241 // Currently enabling asserts breaks auto-test-initial in IE. | |
1242 //_WebAnimationsTestingUtilities._enableAsserts(); | |
1243 } | |
1244 }); | |
1245 | |
1246 // Don't export the timing functions in unittests. | |
1247 if (testType() != 'unit') { | |
1248 addEventListener('load', testharness_timeline_setup); | |
1249 | |
1250 window.at = at; | |
1251 window.timing_test = timing_test; | |
1252 window.test = test_without_at; | |
1253 | |
1254 // Expose the extra API | |
1255 window.testharness_timeline = new TestTimeline(); | |
1256 | |
1257 // Override existing timing functions | |
1258 window.requestAnimationFrame = | |
1259 testharness_timeline.requestAnimationFrame.bind(testharness_timeline); | |
1260 window.performance.now = null; | |
1261 window.Date.now = testharness_timeline.now.bind(testharness_timeline); | |
1262 } | |
1263 | |
1264 window.inExploreMode = inExploreMode; | |
1265 | |
1266 })(); | |
OLD | NEW |