1/*
2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/*
32 * This script is intended to be used for constructing layout tests which
33 * exercise the interpolation functionaltiy of the animation system.
34 * Tests which run using this script should be portable across browsers.
35 *
36 * The following function is exported:
37 *  * assertInterpolation({property: x, from: y, to: z}, [{at: fraction, is: value}])
38 *    Constructs a test case which for each fraction will output a PASS
39 *    or FAIL depending on whether the interpolated result matches
40 *    'value'. Replica elements are constructed to aid eyeballing test
41 *    results.
42 */
43'use strict';
44(function() {
45  var endEvent = 'animationend';
46  var testCount = 0;
47  var animationEventCount = 0;
48  var durationSeconds = 0;
49  var iterationCount = 0.5;
50  var delaySeconds = 0;
51  var fragment = document.createDocumentFragment();
52  var fragmentAttachedListeners = [];
53  var style = document.createElement('style');
54  var afterTestCallback = null;
55  fragment.appendChild(style);
56
57  var tests = document.createElement('div');
58  tests.id = 'interpolation-tests';
59  tests.textContent = 'Interpolation Tests:';
60  fragment.appendChild(tests);
61
62  var updateScheduled = false;
63  function maybeScheduleUpdate() {
64    if (updateScheduled) {
65      return;
66    }
67    updateScheduled = true;
68    setTimeout(function() {
69      updateScheduled = false;
70      document.body.appendChild(fragment);
71      fragmentAttachedListeners.forEach(function(listener) {listener();});
72    }, 0);
73  }
74
75  function evaluateTests() {
76    var targets = document.querySelectorAll('.target.active');
77    for (var i = 0; i < targets.length; i++) {
78      targets[i].evaluate();
79    }
80  }
81
82  function afterTest(callback) {
83    afterTestCallback = callback;
84  }
85
86  // Constructs a timing function which produces 'y' at x = 0.5
87  function createEasing(y) {
88    // FIXME: if 'y' is > 0 and < 1 use a linear timing function and allow
89    // 'x' to vary. Use a bezier only for values < 0 or > 1.
90    if (y == 0) {
91      return 'steps(1, end)';
92    }
93    if (y == 1) {
94      return 'steps(1, start)';
95    }
96    if (y == 0.5) {
97      return 'steps(2, end)';
98    }
99    // Approximate using a bezier.
100    var b = (8 * y - 1) / 6;
101    return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')';
102  }
103
104  function createTestContainer(description, className) {
105    var testContainer = document.createElement('div');
106    testContainer.setAttribute('description', description);
107    testContainer.classList.add('test');
108    if (className) {
109      testContainer.classList.add(className);
110    }
111    return testContainer;
112  }
113
114  function convertPropertyToCamelCase(property) {
115    return property.replace(/^-/, '').replace(/-\w/g, function(m) {return m[1].toUpperCase();});
116  }
117
118  function describeTest(params) {
119    return convertPropertyToCamelCase(params.property) + ': from [' + params.from + '] to [' + params.to + ']';
120  }
121
122  var nextKeyframeId = 0;
123  function assertInterpolation(params, expectations) {
124    var testId = 'test-' + ++nextKeyframeId;
125    var nextCaseId = 0;
126    var testContainer = createTestContainer(describeTest(params), testId);
127    tests.appendChild(testContainer);
128    expectations.forEach(function(expectation) {
129        testContainer.appendChild(makeInterpolationTest(
130            expectation.at, testId, 'case-' + ++nextCaseId, params, expectation.is));
131    });
132    maybeScheduleUpdate();
133  }
134
135  function roundNumbers(value) {
136    // Round numbers to two decimal places.
137    return value.replace(/-?\d*\.\d+/g, function(n) {
138        return (parseFloat(n).toFixed(2)).
139            replace(/\.\d+/, function(m) {
140              return m.replace(/0+$/, '');
141            }).
142            replace(/\.$/, '').
143            replace(/^-0$/, '0');
144      });
145  }
146
147  function normalizeValue(value) {
148    return roundNumbers(value).
149        // Place whitespace between tokens.
150        replace(/([\w\d.]+|[^\s])/g, '$1 ').
151        replace(/\s+/g, ' ');
152  }
153
154  function createTargetContainer(id) {
155    var targetContainer = document.createElement('div');
156    var template = document.querySelector('#target-template');
157    if (template) {
158      if (template.content)
159        targetContainer.appendChild(template.content.cloneNode(true));
160      else if (template.querySelector('div'))
161        targetContainer.appendChild(template.querySelector('div').cloneNode(true));
162      else
163        targetContainer.appendChild(template.cloneNode(true));
164      // Remove whitespace text nodes at start / end.
165      while (targetContainer.firstChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.firstChild.nodeValue)) {
166        targetContainer.removeChild(targetContainer.firstChild);
167      }
168      while (targetContainer.lastChild.nodeType != Node.ELEMENT_NODE && !/\S/.test(targetContainer.lastChild.nodeValue)) {
169        targetContainer.removeChild(targetContainer.lastChild);
170      }
171      // If the template contains just one element, use that rather than a wrapper div.
172      if (targetContainer.children.length == 1 && targetContainer.childNodes.length == 1) {
173        targetContainer = targetContainer.firstChild;
174        targetContainer.parentNode.removeChild(targetContainer);
175      }
176    }
177    var target = targetContainer.querySelector('.target') || targetContainer;
178    target.classList.add('target');
179    target.classList.add(id);
180    return targetContainer;
181  }
182
183  function sanitizeUrls(value) {
184    var matches = value.match(/url\([^\)]*\)/g);
185    if (matches !== null) {
186      for (var i = 0; i < matches.length; ++i) {
187        var url = /url\(([^\)]*)\)/g.exec(matches[i])[1];
188        var anchor = document.createElement('a');
189        anchor.href = url;
190        anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/'));
191        value = value.replace(matches[i], 'url(' + anchor.href + ')');
192      }
193    }
194    return value;
195  }
196
197  function makeInterpolationTest(fraction, testId, caseId, params, expectation) {
198    var t = async_test(describeTest(params) + ' at ' + fraction);
199    var targetContainer = createTargetContainer(caseId);
200    var target = targetContainer.querySelector('.target') || targetContainer;
201    target.classList.add('active');
202    var replicaContainer, replica;
203    replicaContainer = createTargetContainer(caseId);
204    replica = replicaContainer.querySelector('.target') || replicaContainer;
205    replica.classList.add('replica');
206    replica.style.setProperty(params.property, expectation);
207    if (params.prefixedProperty) {
208      for (var i = 0; i < params.prefixedProperty.length; i++) {
209        replica.style.setProperty(params.prefixedProperty[i], expectation);
210      }
211    }
212
213    target.evaluate = function() {
214      var target = this;
215      t.step(function() {
216        window.CSS && assert_true(CSS.supports(params.property, expectation));
217        var value = getComputedStyle(target).getPropertyValue(params.property);
218        var property = params.property;
219        if (params.prefixedProperty) {
220          var i = 0;
221          while (i < params.prefixedProperty.length && !value) {
222            property = params.prefixedProperty[i++];
223            value = getComputedStyle(target).getPropertyValue(property)
224          }
225        }
226        if (!value) {
227          assert_false(params.property + ' not supported by this browser');
228        }
229        var originalValue = value;
230        var parsedExpectation = getComputedStyle(replica).getPropertyValue(property);
231        assert_equals(normalizeValue(originalValue), normalizeValue(parsedExpectation));
232        t.done();
233      });
234    };
235
236    var easing = createEasing(fraction);
237    testCount++;
238    var keyframes = [{}, {}];
239    keyframes[0][convertPropertyToCamelCase(params.property)] = params.from;
240    keyframes[1][convertPropertyToCamelCase(params.property)] = params.to;
241    fragmentAttachedListeners.push(function() {
242      target.animate(keyframes, {
243          fill: 'forwards',
244          duration: 1,
245          easing: easing,
246          delay: -0.5,
247          iterations: 0.5,
248        });
249      animationEnded();
250    });
251    var testFragment = document.createDocumentFragment();
252    testFragment.appendChild(targetContainer);
253    replica && testFragment.appendChild(replicaContainer);
254    testFragment.appendChild(document.createTextNode('\n'));
255    return testFragment;
256  }
257
258  var finished = false;
259  function finishTest() {
260    finished = true;
261    evaluateTests();
262    if (afterTestCallback) {
263      afterTestCallback();
264    }
265    if (window.testRunner) {
266      var results = document.querySelector('#results');
267      document.documentElement.textContent = '';
268      document.documentElement.appendChild(results);
269      testRunner.dumpAsText();
270      testRunner.notifyDone();
271    }
272  }
273
274  if (window.testRunner) {
275    testRunner.waitUntilDone();
276  }
277
278  function isLastAnimationEvent() {
279    return !finished && animationEventCount === testCount;
280  }
281
282  function animationEnded() {
283    animationEventCount++;
284    if (!isLastAnimationEvent()) {
285      return;
286    }
287    finishTest();
288  }
289
290  document.documentElement.addEventListener(endEvent, animationEnded);
291
292  if (!window.testRunner) {
293    setTimeout(function() {
294      if (finished) {
295        return;
296      }
297      finishTest();
298    }, 10000);
299  }
300
301  window.assertInterpolation = assertInterpolation;
302  window.afterTest = afterTest;
303})();
304