Home | History | Annotate | Download | only in resources
      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