tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

interpolation-testcommon.js (22606B)


      1 'use strict';
      2 (function() {
      3  var interpolationTests = [];
      4  var compositionTests = [];
      5  var cssAnimationsData = {
      6    sharedStyle: null,
      7    nextID: 0,
      8  };
      9  var expectNoInterpolation = {};
     10  var expectNotAnimatable = {};
     11  var neutralKeyframe = {};
     12  function isNeutralKeyframe(keyframe) {
     13    return keyframe === neutralKeyframe;
     14  }
     15 
     16  // For the CSS interpolation methods set the delay to be negative half the
     17  // duration, so we are immediately at the halfway point of the animation.
     18  // We then use an easing function that maps halfway to whatever progress
     19  // we actually want.
     20 
     21  var cssAnimationsInterpolation = {
     22    name: 'CSS Animations',
     23    isSupported: function() {return true;},
     24    supportsProperty: function() {return true;},
     25    supportsValue: function() {return true;},
     26    setup: function() {},
     27    nonInterpolationExpectations: function(from, to) {
     28      return expectFlip(from, to, 0.5);
     29    },
     30    notAnimatableExpectations: function(from, to, underlying) {
     31      return expectFlip(underlying, underlying, -Infinity);
     32    },
     33    interpolate: function(property, from, to, at, target) {
     34      var id = cssAnimationsData.nextID++;
     35      if (!cssAnimationsData.sharedStyle) {
     36        cssAnimationsData.sharedStyle = createElement(document.body, 'style');
     37      }
     38      cssAnimationsData.sharedStyle.textContent += '' +
     39        '@keyframes animation' + id + ' {' +
     40          (isNeutralKeyframe(from) ? '' : `from {${property}:${from};}`) +
     41          (isNeutralKeyframe(to) ? '' : `to {${property}:${to};}`) +
     42        '}';
     43      target.style.animationName = 'animation' + id;
     44      target.style.animationDuration = '100s';
     45      target.style.animationDelay = '-50s';
     46      target.style.animationTimingFunction = createEasing(at);
     47    },
     48    interpolateWithComposition: function(property, from, fromComposite, to, toComposite, at, target) {
     49      const id = cssAnimationsData.nextID++;
     50      if (!cssAnimationsData.sharedStyle) {
     51        cssAnimationsData.sharedStyle = createElement(document.body, 'style');
     52      }
     53      cssAnimationsData.sharedStyle.textContent += '' +
     54        '@keyframes animation' + id + ' {' +
     55          (isNeutralKeyframe(from)
     56              ? '' : `from {${property}:${from};animation-composition:${fromComposite}}`) +
     57          (isNeutralKeyframe(to)
     58              ? '' : `to {${property}:${to};animation-composition:${toComposite}}`) +
     59        '}';
     60      target.style.animationName = 'animation' + id;
     61      target.style.animationDuration = '100s';
     62      target.style.animationDelay = '-50s';
     63      target.style.animationTimingFunction = createEasing(at);
     64    },
     65  };
     66 
     67  var cssTransitionsInterpolation = {
     68    name: 'CSS Transitions',
     69    isSupported: function() {return true;},
     70    supportsProperty: function() {return true;},
     71    supportsValue: function() {return true;},
     72    setup: function(property, from, target) {
     73      target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from);
     74    },
     75    nonInterpolationExpectations: function(from, to) {
     76      return expectFlip(from, to, -Infinity);
     77    },
     78    notAnimatableExpectations: function(from, to, underlying) {
     79      return expectFlip(from, to, -Infinity);
     80    },
     81    interpolate: function(property, from, to, at, target, behavior) {
     82      // Force a style recalc on target to set the 'from' value.
     83      getComputedStyle(target).getPropertyValue(property);
     84      target.style.transitionDuration = '100s';
     85      target.style.transitionDelay = '-50s';
     86      target.style.transitionTimingFunction = createEasing(at);
     87      target.style.transitionProperty = property;
     88      if (behavior) {
     89        target.style.transitionBehavior = behavior;
     90      }
     91      target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to);
     92    },
     93  };
     94 
     95  var cssTransitionAllInterpolation = {
     96    name: 'CSS Transitions with transition: all',
     97    isSupported: function() {return true;},
     98    // The 'all' value doesn't cover custom properties.
     99    supportsProperty: function(property) {return property.indexOf('--') !== 0;},
    100    supportsValue: function() {return true;},
    101    setup: function(property, from, target) {
    102      target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from);
    103    },
    104    nonInterpolationExpectations: function(from, to) {
    105      return expectFlip(from, to, -Infinity);
    106    },
    107    notAnimatableExpectations: function(from, to, underlying) {
    108      return expectFlip(from, to, -Infinity);
    109    },
    110    interpolate: function(property, from, to, at, target, behavior) {
    111      // Force a style recalc on target to set the 'from' value.
    112      getComputedStyle(target).getPropertyValue(property);
    113      target.style.transitionDuration = '100s';
    114      target.style.transitionDelay = '-50s';
    115      target.style.transitionTimingFunction = createEasing(at);
    116      target.style.transitionProperty = 'all';
    117      if (behavior) {
    118        target.style.transitionBehavior = behavior;
    119      }
    120      target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to);
    121    },
    122  };
    123 
    124  var cssTransitionsInterpolationAllowDiscrete = {
    125    name: 'CSS Transitions with transition-behavior:allow-discrete',
    126    isSupported: function() {return true;},
    127    supportsProperty: function() {return true;},
    128    supportsValue: function() {return true;},
    129    setup: function(property, from, target) {
    130      target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from);
    131    },
    132    nonInterpolationExpectations: function(from, to) {
    133      return expectFlip(from, to, 0.5);
    134    },
    135    notAnimatableExpectations: function(from, to, underlying) {
    136      return expectFlip(from, to, -Infinity);
    137    },
    138    interpolate: function(property, from, to, at, target, behavior) {
    139      // Force a style recalc on target to set the 'from' value.
    140      getComputedStyle(target).getPropertyValue(property);
    141      target.style.transitionDuration = '100s';
    142      target.style.transitionDelay = '-50s';
    143      target.style.transitionTimingFunction = createEasing(at);
    144      target.style.transitionProperty = property;
    145      target.style.transitionBehavior = 'allow-discrete';
    146      target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to);
    147    },
    148  };
    149 
    150  var cssTransitionAllInterpolationAllowDiscrete = {
    151    name: 'CSS Transitions with transition-property:all and transition-behavor:allow-discrete',
    152    isSupported: function() {return true;},
    153    // The 'all' value doesn't cover custom properties.
    154    supportsProperty: function(property) {return property.indexOf('--') !== 0;},
    155    supportsValue: function() {return true;},
    156    setup: function(property, from, target) {
    157      target.style.setProperty(property, isNeutralKeyframe(from) ? '' : from);
    158    },
    159    nonInterpolationExpectations: function(from, to) {
    160      return expectFlip(from, to, 0.5);
    161    },
    162    notAnimatableExpectations: function(from, to, underlying) {
    163      return expectFlip(from, to, -Infinity);
    164    },
    165    interpolate: function(property, from, to, at, target, behavior) {
    166      // Force a style recalc on target to set the 'from' value.
    167      getComputedStyle(target).getPropertyValue(property);
    168      target.style.transitionDuration = '100s';
    169      target.style.transitionDelay = '-50s';
    170      target.style.transitionTimingFunction = createEasing(at);
    171      target.style.transitionProperty = 'all';
    172      target.style.transitionBehavior = 'allow-discrete';
    173      target.style.setProperty(property, isNeutralKeyframe(to) ? '' : to);
    174    },
    175  };
    176 
    177  var webAnimationsInterpolation = {
    178    name: 'Web Animations',
    179    isSupported: function() {return 'animate' in Element.prototype;},
    180    supportsProperty: function(property) {return true;},
    181    supportsValue: function(value) {return value !== '';},
    182    setup: function() {},
    183    nonInterpolationExpectations: function(from, to) {
    184      return expectFlip(from, to, 0.5);
    185    },
    186    notAnimatableExpectations: function(from, to, underlying) {
    187      return expectFlip(underlying, underlying, -Infinity);
    188    },
    189    interpolate: function(property, from, to, at, target) {
    190      this.interpolateWithComposition(property, from, 'replace', to, 'replace', at, target);
    191    },
    192    interpolateWithComposition: function(property, from, fromComposite, to, toComposite, at, target) {
    193      // This case turns into a test error later on.
    194      if (!this.isSupported())
    195        return;
    196 
    197      // Convert standard properties to camelCase.
    198      if (!property.startsWith('--')) {
    199        for (var i = property.length - 2; i > 0; --i) {
    200          if (property[i] === '-') {
    201            property = property.substring(0, i) + property[i + 1].toUpperCase() + property.substring(i + 2);
    202          }
    203        }
    204        if (property === 'offset') {
    205          property = 'cssOffset';
    206        } else if (property === 'float') {
    207          property = 'cssFloat';
    208        }
    209      }
    210      var keyframes = [];
    211      if (!isNeutralKeyframe(from)) {
    212        keyframes.push({
    213          offset: 0,
    214          composite: fromComposite,
    215          [property]: from,
    216        });
    217      }
    218      if (!isNeutralKeyframe(to)) {
    219        keyframes.push({
    220          offset: 1,
    221          composite: toComposite,
    222          [property]: to,
    223        });
    224      }
    225      var animation = target.animate(keyframes, {
    226        fill: 'forwards',
    227        duration: 100 * 1000,
    228        easing: createEasing(at),
    229      });
    230      animation.pause();
    231      animation.currentTime = 50 * 1000;
    232    },
    233  };
    234 
    235  function expectFlip(from, to, flipAt) {
    236    return [-0.3, 0, 0.3, 0.5, 0.6, 1, 1.5].map(function(at) {
    237      return {
    238        at: at,
    239        expect: at < flipAt ? from : to
    240      };
    241    });
    242  }
    243 
    244  // Constructs a timing function which produces 'y' at x = 0.5
    245  function createEasing(y) {
    246    if (y == 0) {
    247      return 'steps(1, end)';
    248    }
    249    if (y == 1) {
    250      return 'steps(1, start)';
    251    }
    252    if (y == 0.5) {
    253      return 'linear';
    254    }
    255    // Approximate using a bezier.
    256    var b = (8 * y - 1) / 6;
    257    return 'cubic-bezier(0, ' + b + ', 1, ' + b + ')';
    258  }
    259 
    260  function createElement(parent, tag, text) {
    261    var element = document.createElement(tag || 'div');
    262    element.textContent = text || '';
    263    parent.appendChild(element);
    264    return element;
    265  }
    266 
    267  function createTargetContainer(parent, className) {
    268    var targetContainer = createElement(parent);
    269    targetContainer.classList.add('container');
    270    var template = document.querySelector('#target-template');
    271    if (template) {
    272      targetContainer.appendChild(template.content.cloneNode(true));
    273    }
    274    var target = targetContainer.querySelector('.target') || targetContainer;
    275    target.classList.add('target', className);
    276    target.parentElement.classList.add('parent');
    277    targetContainer.target = target;
    278    return targetContainer;
    279  }
    280 
    281  function roundNumbers(value) {
    282    return value.
    283        // Round numbers to two decimal places.
    284        replace(/-?\d*\.\d+(e-?\d+)?/g, function(n) {
    285          return (parseFloat(n).toFixed(2)).
    286              replace(/\.\d+/, function(m) {
    287                return m.replace(/0+$/, '');
    288              }).
    289              replace(/\.$/, '').
    290              replace(/^-0$/, '0');
    291        });
    292  }
    293 
    294  var anchor = document.createElement('a');
    295  function sanitizeUrls(value) {
    296    var matches = value.match(/url\("([^#][^\)]*)"\)/g);
    297    if (matches !== null) {
    298      for (var i = 0; i < matches.length; ++i) {
    299        var url = /url\("([^#][^\)]*)"\)/g.exec(matches[i])[1];
    300        anchor.href = url;
    301        anchor.pathname = '...' + anchor.pathname.substring(anchor.pathname.lastIndexOf('/'));
    302        value = value.replace(matches[i], 'url(' + anchor.href + ')');
    303      }
    304    }
    305    return value;
    306  }
    307 
    308  function normalizeValue(value) {
    309    return roundNumbers(sanitizeUrls(value)).
    310        // Place whitespace between tokens.
    311        replace(/([\w\d.]+|[^\s])/g, '$1 ').
    312        replace(/\s+/g, ' ');
    313  }
    314 
    315  function stringify(text) {
    316    if (!text.includes("'")) {
    317      return `'${text}'`;
    318    }
    319    return `"${text.replace('"', '\\"')}"`;
    320  }
    321 
    322  function keyframeText(keyframe) {
    323    return isNeutralKeyframe(keyframe) ? 'neutral' : `[${keyframe}]`;
    324  }
    325 
    326  function keyframeCode(keyframe) {
    327    return isNeutralKeyframe(keyframe) ? 'neutralKeyframe' : `${stringify(keyframe)}`;
    328  }
    329 
    330  function createInterpolationTestTargets(interpolationMethod, interpolationMethodContainer, interpolationTest) {
    331    var property = interpolationTest.options.property;
    332    var from = interpolationTest.options.from;
    333    var to = interpolationTest.options.to;
    334    let underlying = interpolationTest.options.underlying;
    335    var comparisonFunction = interpolationTest.options.comparisonFunction;
    336    var behavior = interpolationTest.options.behavior;
    337 
    338    if ((interpolationTest.options.method && interpolationTest.options.method != interpolationMethod.name)
    339      || !interpolationMethod.supportsProperty(property)
    340      || !interpolationMethod.supportsValue(from)
    341      || !interpolationMethod.supportsValue(to)) {
    342      return;
    343    }
    344 
    345    var testText = `${interpolationMethod.name}: property <${property}> from ${keyframeText(from)} to ${keyframeText(to)}`;
    346    var testContainer = createElement(interpolationMethodContainer, 'div');
    347    createElement(testContainer);
    348    var expectations = interpolationTest.expectations;
    349    var applyUnderlying = false;
    350    if (expectations === expectNoInterpolation) {
    351      expectations = interpolationMethod.nonInterpolationExpectations(from, to);
    352    } else if (expectations === expectNotAnimatable) {
    353      expectations = interpolationMethod.notAnimatableExpectations(from, to, interpolationTest.options.underlying);
    354      applyUnderlying = true;
    355    } else if (interpolationTest.options[interpolationMethod.name]) {
    356      expectations = interpolationTest.options[interpolationMethod.name];
    357    }
    358 
    359    // Setup a standard equality function if an override is not provided.
    360    if (!comparisonFunction) {
    361      comparisonFunction = (actual, expected) => {
    362        assert_equals(normalizeValue(actual), normalizeValue(expected));
    363      };
    364    }
    365 
    366    return expectations.map(function(expectation) {
    367      var actualTargetContainer = createTargetContainer(testContainer, 'actual');
    368      var expectedTargetContainer = createTargetContainer(testContainer, 'expected');
    369      var expectedProperties = expectation.option || expectation.expect;
    370      if (typeof expectedProperties !== "object") {
    371        expectedProperties = {[property]: expectedProperties};
    372      }
    373      var target = actualTargetContainer.target;
    374      if (applyUnderlying) {
    375        assert_true(typeof underlying !== 'undefined', '\'underlying\' value must be provided');
    376        target.style.setProperty(property, underlying);
    377      }
    378      interpolationMethod.setup(property, from, target);
    379      target.interpolate = function() {
    380        interpolationMethod.interpolate(property, from, to, expectation.at, target, behavior);
    381      };
    382      target.measure = function() {
    383        for (var [expectedProp, expectedStr] of Object.entries(expectedProperties)) {
    384          if (!isNeutralKeyframe(expectedStr)) {
    385            expectedTargetContainer.target.style.setProperty(expectedProp, expectedStr);
    386          }
    387          var expectedValue = getComputedStyle(expectedTargetContainer.target).getPropertyValue(expectedProp);
    388          let testName = `${testText} at (${expectation.at}) should be [${sanitizeUrls(expectedStr)}]`;
    389          if (property !== expectedProp) {
    390            testName += ` for <${expectedProp}>`;
    391          }
    392          test(function() {
    393            assert_true(interpolationMethod.isSupported(), `${interpolationMethod.name} should be supported`);
    394 
    395            if (from && from !== neutralKeyframe) {
    396              assert_true(CSS.supports(property, from), '\'from\' value should be supported');
    397            }
    398            if (to && to !== neutralKeyframe) {
    399              assert_true(CSS.supports(property, to), '\'to\' value should be supported');
    400            }
    401            if (typeof underlying !== 'undefined') {
    402              assert_true(CSS.supports(property, underlying), '\'underlying\' value should be supported');
    403            }
    404 
    405            comparisonFunction(
    406                getComputedStyle(target).getPropertyValue(expectedProp),
    407                expectedValue);
    408          }, testName);
    409        }
    410      };
    411      return target;
    412    });
    413  }
    414 
    415  function createCompositionTestTargets(compositionMethod, compositionMethodContainer, compositionTest) {
    416    var options = compositionTest.options;
    417    var property = options.property;
    418    var underlying = options.underlying;
    419    var comparisonFunction = options.comparisonFunction;
    420    var from = options.accumulateFrom || options.addFrom || options.replaceFrom;
    421    var to = options.accumulateTo || options.addTo || options.replaceTo;
    422    var fromComposite = 'accumulateFrom' in options ? 'accumulate' : 'addFrom' in options ? 'add' : 'replace';
    423    var toComposite = 'accumulateTo' in options ? 'accumulate' : 'addTo' in options ? 'add' : 'replace';
    424    const invalidFrom = 'addFrom' in options === 'replaceFrom' in options
    425        && 'addFrom' in options === 'accumulateFrom' in options;
    426    const invalidTo = 'addTo' in options === 'replaceTo' in options
    427        && 'addTo' in options === 'accumulateTo' in options;
    428    if (invalidFrom || invalidTo) {
    429      test(function() {
    430        assert_false(invalidFrom, 'Exactly one of accumulateFrom, addFrom, or replaceFrom must be specified');
    431        assert_false(invalidTo, 'Exactly one of accumulateTo, addTo, or replaceTo must be specified');
    432      }, `Composition tests must have valid setup`);
    433    }
    434 
    435    var testText = `Compositing ${compositionMethod.name}: property <${property}> underlying [${underlying}] from ${fromComposite} [${from}] to ${toComposite} [${to}]`;
    436    var testContainer = createElement(compositionMethodContainer, 'div');
    437    createElement(testContainer);
    438 
    439    // Setup a standard equality function if an override is not provided.
    440    if (!comparisonFunction) {
    441      comparisonFunction = (actual, expected) => {
    442        assert_equals(normalizeValue(actual), normalizeValue(expected));
    443      };
    444    }
    445 
    446    return compositionTest.expectations.map(function(expectation) {
    447      var actualTargetContainer = createTargetContainer(testContainer, 'actual');
    448      var expectedTargetContainer = createTargetContainer(testContainer, 'expected');
    449      var expectedStr = expectation.option || expectation.expect;
    450      if (!isNeutralKeyframe(expectedStr)) {
    451        expectedTargetContainer.target.style.setProperty(property, expectedStr);
    452      }
    453      var target = actualTargetContainer.target;
    454      target.style.setProperty(property, underlying);
    455      target.interpolate = function() {
    456        compositionMethod.interpolateWithComposition(property, from, fromComposite, to, toComposite, expectation.at, target);
    457      };
    458      target.measure = function() {
    459        var expectedValue = getComputedStyle(expectedTargetContainer.target).getPropertyValue(property);
    460        test(function() {
    461 
    462          if (from && from !== neutralKeyframe) {
    463            assert_true(CSS.supports(property, from), '\'from\' value should be supported');
    464          }
    465          if (to && to !== neutralKeyframe) {
    466            assert_true(CSS.supports(property, to), '\'to\' value should be supported');
    467          }
    468          if (typeof underlying !== 'undefined') {
    469            assert_true(CSS.supports(property, underlying), '\'underlying\' value should be supported');
    470          }
    471 
    472          comparisonFunction(
    473              getComputedStyle(target).getPropertyValue(property),
    474              expectedValue);
    475        }, `${testText} at (${expectation.at}) should be [${sanitizeUrls(expectedStr)}]`);
    476      };
    477      return target;
    478    });
    479  }
    480 
    481 
    482 
    483  function createTestTargets(interpolationMethods, interpolationTests, compositionMethods, compositionTests, container) {
    484    var targets = [];
    485    for (var interpolationMethod of interpolationMethods) {
    486      var interpolationMethodContainer = createElement(container);
    487      for (var interpolationTest of interpolationTests) {
    488        if(!interpolationTest.options.target_names ||
    489           interpolationTest.options.target_names.includes(interpolationMethod.name)) {
    490            [].push.apply(targets, createInterpolationTestTargets(interpolationMethod, interpolationMethodContainer, interpolationTest));
    491          }
    492      }
    493    }
    494    for (var compositionMethod of compositionMethods) {
    495      var compositionContainer = createElement(container);
    496      for (var compositionTest of compositionTests) {
    497        [].push.apply(targets, createCompositionTestTargets(compositionMethod, compositionContainer, compositionTest));
    498      }
    499    }
    500    return targets;
    501  }
    502 
    503  function test_no_interpolation(options) {
    504    test_interpolation(options, expectNoInterpolation);
    505  }
    506  function test_not_animatable(options) {
    507    test_interpolation(options, expectNotAnimatable);
    508  }
    509  function create_tests(addAllowDiscreteTests) {
    510    var interpolationMethods = [
    511      cssTransitionsInterpolation,
    512      cssTransitionAllInterpolation,
    513      cssAnimationsInterpolation,
    514      webAnimationsInterpolation,
    515    ];
    516    var compositionMethods = [
    517      cssAnimationsInterpolation,
    518      webAnimationsInterpolation,
    519    ];
    520    if (addAllowDiscreteTests) {
    521      interpolationMethods = [
    522        cssTransitionsInterpolationAllowDiscrete,
    523        cssTransitionAllInterpolationAllowDiscrete,
    524      ].concat(interpolationMethods);
    525    }
    526    var container = createElement(document.body);
    527    var targets = createTestTargets(interpolationMethods, interpolationTests,
    528                                    compositionMethods, compositionTests,
    529                                    container);
    530    // Separate interpolation and measurement into different phases to avoid O(n^2) of the number of targets.
    531    for (var target of targets) {
    532      target.interpolate();
    533    }
    534    for (var target of targets) {
    535      target.measure();
    536    }
    537    container.remove();
    538  }
    539 
    540  function test_interpolation(options, expectations) {
    541    interpolationTests.push({options, expectations});
    542    create_tests(expectations === expectNoInterpolation || expectations === expectNotAnimatable);
    543    interpolationTests = [];
    544  }
    545  function test_composition(options, expectations) {
    546    compositionTests.push({options, expectations});
    547    create_tests();
    548    compositionTests = [];
    549  }
    550  window.test_interpolation = test_interpolation;
    551  window.test_no_interpolation = test_no_interpolation;
    552  window.test_not_animatable = test_not_animatable;
    553  window.test_composition = test_composition;
    554  window.neutralKeyframe = neutralKeyframe;
    555  window.roundNumbers = roundNumbers;
    556  window.normalizeValue = normalizeValue;
    557 })();