<!DOCTYPE html>
<meta charset=utf-8>
<title>KeyframeEffectReadOnly constructor tests</title>
<link rel="help" href="https://w3c.github.io/web-animations/#processing-a-keyframes-argument">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../testcommon.js"></script>
<script src="../../resources/keyframe-utils.js"></script>
<body>
<div id="log"></div>
<div id="target"></div>
<script>
'use strict';

// Test the "process a keyframe-like object" procedure.
//
// This file only tests the KeyframeEffectReadOnly constructor since it is
// assumed that the implementation of the KeyframeEffect constructor,
// Animatable.animate() method, and KeyframeEffect.setKeyframes() method will
// all share common machinery and it is not necessary to test each method.

// Test that only animatable properties are accessed

var gNonAnimatableProps = [
  'animation', // Shorthands where all the longhand sub-properties are not
               // animatable, are also not animatable.
  'animationDelay',
  'animationDirection',
  'animationDuration',
  'animationFillMode',
  'animationIterationCount',
  'animationName',
  'animationPlayState',
  'animationTimingFunction',
  'transition',
  'transitionDelay',
  'transitionDuration',
  'transitionProperty',
  'transitionTimingFunction',
  'display',
  'unsupportedProperty',
];

function TestKeyframe(testProp) {
  var _propAccessCount = 0;

  Object.defineProperty(this, testProp, {
    get: function() { _propAccessCount++; },
    enumerable: true
  });

  Object.defineProperty(this, 'propAccessCount', {
    get: function() { return _propAccessCount; }
  });
}

function GetTestKeyframeSequence(testProp) {
  return [ new TestKeyframe(testProp) ]
}

gNonAnimatableProps.forEach(function(prop) {
  test(function(t) {
    var testKeyframe = new TestKeyframe(prop);

    new KeyframeEffectReadOnly(null, testKeyframe);

    assert_equals(testKeyframe.propAccessCount, 0, 'Accessor not called');
  }, 'non-animatable property \'' + prop + '\' is not accessed when using'
     + ' a property-indexed keyframe object');
});

gNonAnimatableProps.forEach(function(prop) {
  test(function(t) {
    var testKeyframes = GetTestKeyframeSequence(prop);

    new KeyframeEffectReadOnly(null, testKeyframes);

    assert_equals(testKeyframes[0].propAccessCount, 0, 'Accessor not called');
  }, 'non-animatable property \'' + prop + '\' is not accessed when using'
     + ' a keyframe sequence');
});

// Test equivalent forms of property indexed and sequenced keyframe syntax

function assertEquivalentKeyframeSyntax(keyframesA, keyframesB) {
  var processedKeyframesA = new KeyframeEffectReadOnly(null, keyframesA).getKeyframes();
  var processedKeyframesB = new KeyframeEffectReadOnly(null, keyframesB).getKeyframes();
  assert_frame_lists_equal(processedKeyframesA, processedKeyframesB);
}

var gEquivalentSyntaxTests = [
  {
    description: 'two properties with one value',
    indexedKeyframes: {
      left: '100px',
      opacity: ['1'],
    },
    sequencedKeyframes: [
      {left: '100px', opacity: '1'},
    ],
  },
  {
    description: 'two properties with three values',
    indexedKeyframes: {
      left: ['10px', '100px', '150px'],
      opacity: ['1', '0', '1'],
    },
    sequencedKeyframes: [
      {left: '10px', opacity: '1'},
      {left: '100px', opacity: '0'},
      {left: '150px', opacity: '1'},
    ],
  },
  {
    description: 'two properties with different numbers of values',
    indexedKeyframes: {
      left: ['0px', '100px', '200px'],
      opacity: ['0', '1']
    },
    sequencedKeyframes: [
      {left: '0px', opacity: '0'},
      {left: '100px'},
      {left: '200px', opacity: '1'},
    ],
  },
  {
    description: 'same offset applied to all keyframes',
    indexedKeyframes: {
      left: ['0px', '100px'],
      offset: 0.5,
    },
    sequencedKeyframes: [
      {left: '0px', offset: 0.5},
      {left: '100px', offset: 0.5},
    ],
  },
  {
    description: 'same easing applied to all keyframes',
    indexedKeyframes: {
      left: ['10px', '100px', '150px'],
      opacity: ['1', '0', '1'],
      easing: 'ease',
    },
    sequencedKeyframes: [
      {left: '10px', opacity: '1', easing: 'ease'},
      {left: '100px', opacity: '0', easing: 'ease'},
      {left: '150px', opacity: '1', easing: 'ease'},
    ],
  },
  {
    description: 'same composite applied to all keyframes',
    indexedKeyframes: {
      left: ['0px', '100px'],
      composite: 'add',
    },
    sequencedKeyframes: [
      {left: '0px', composite: 'add'},
      {left: '100px', composite: 'add'},
    ],
  },
];

gEquivalentSyntaxTests.forEach(function({description, indexedKeyframes, sequencedKeyframes}) {
  test(function(t) {
    assertEquivalentKeyframeSyntax(indexedKeyframes, sequencedKeyframes);
  }, 'Equivalent property indexed and sequenced keyframes: ' + description);
});

// Test handling of custom iterable objects.

function createIterable(iterations) {
  return {
    [Symbol.iterator]() {
      var i = 0;
      return {
        next() {
          return iterations[i++];
        },
      };
    },
  };
}

test(() => {
  var effect = new KeyframeEffect(null, createIterable([
    {done: false, value: {left: '100px'}},
    {done: false, value: {left: '300px'}},
    {done: false, value: {left: '200px'}},
    {done: true},
  ]));
  assert_frame_lists_equal(effect.getKeyframes(), [
    {offset: null, computedOffset: 0, easing: 'linear', left: '100px'},
    {offset: null, computedOffset: 0.5, easing: 'linear', left: '300px'},
    {offset: null, computedOffset: 1, easing: 'linear', left: '200px'},
  ]);
}, 'Custom iterator with basic keyframes.');

test(() => {
  var keyframes = createIterable([
    {done: false, value: {left: '100px'}},
    {done: false, value: {left: '300px'}},
    {done: false, value: {left: '200px'}},
    {done: true},
  ]);
  keyframes.easing = 'ease-in-out';
  keyframes.offset = '0.1';
  var effect = new KeyframeEffect(null, keyframes);
  assert_frame_lists_equal(effect.getKeyframes(), [
    {offset: null, computedOffset: 0, easing: 'linear', left: '100px'},
    {offset: null, computedOffset: 0.5, easing: 'linear', left: '300px'},
    {offset: null, computedOffset: 1, easing: 'linear', left: '200px'},
  ]);
}, 'easing and offset are ignored on iterable objects.');

test(() => {
  var effect = new KeyframeEffect(null, createIterable([
    {done: false, value: {left: '100px', top: '200px'}},
    {done: false, value: {left: '300px'}},
    {done: false, value: {left: '200px', top: '100px'}},
    {done: true},
  ]));
  assert_frame_lists_equal(effect.getKeyframes(), [
    {offset: null, computedOffset: 0, easing: 'linear', left: '100px', top: '200px'},
    {offset: null, computedOffset: 0.5, easing: 'linear', left: '300px'},
    {offset: null, computedOffset: 1, easing: 'linear', left: '200px', top: '100px'},
  ]);
}, 'Custom iterator with multiple properties specified.');

test(() => {
  var effect = new KeyframeEffect(null, createIterable([
    {done: false, value: {left: '100px'}},
    {done: false, value: {left: '250px', offset: 0.75}},
    {done: false, value: {left: '200px'}},
    {done: true},
  ]));
  assert_frame_lists_equal(effect.getKeyframes(), [
    {offset: null, computedOffset: 0, easing: 'linear', left: '100px'},
    {offset: 0.75, computedOffset: 0.75, easing: 'linear', left: '250px'},
    {offset: null, computedOffset: 1, easing: 'linear', left: '200px'},
  ]);
}, 'Custom iterator with offset specified.');

test(() => {
  assert_throws({name: 'TypeError'}, function() {
    new KeyframeEffect(null, createIterable([
      {done: false, value: {left: '100px'}},
      {done: false, value: 1234},
      {done: false, value: {left: '200px'}},
      {done: true},
    ]));
  });
}, 'Custom iterator with non object keyframe should throw.');

test(() => {
  var effect = new KeyframeEffect(null, createIterable([
    {done: false, value: {left: ['100px', '200px']}},
    {done: true},
  ]));
  assert_frame_lists_equal(effect.getKeyframes(), [
    {offset: null, computedOffset: 1, easing: 'linear', left: '100px,200px'}
  ]);
}, 'Custom iterator with value list in keyframe should give bizarre string representation of list.');

test(function(t) {
  var keyframe = {};
  Object.defineProperty(keyframe, 'width', {value: '200px'});
  Object.defineProperty(keyframe, 'height', {
    value: '100px',
    enumerable: true});
  assert_equals(keyframe.width, '200px', 'width of keyframe is readable');
  assert_equals(keyframe.height, '100px', 'height of keyframe is readable');
  var anim = createDiv(t).animate([keyframe, {height: '200px'}], 1);
  assert_frame_lists_equal(anim.effect.getKeyframes(), [
      {offset: null, computedOffset: 0, easing: 'linear', height: '100px'},
      {offset: null, computedOffset: 1, easing: 'linear', height: '200px'},
  ]);
}, 'Only enumerable properties on keyframes are considered');

test(function(t) {
  var KeyframeParent = function() { this.width = '100px'; };
  KeyframeParent.prototype = { height: '100px' };
  var Keyframe = function() { this.top = '100px'; };
  Keyframe.prototype = Object.create(KeyframeParent.prototype);
  Object.defineProperty(Keyframe.prototype, 'left', {
    value: '100px',
    enumerable: 'true'});
  var keyframe = new Keyframe();
  var anim = createDiv(t).animate([keyframe, {top: '200px'}], 1);
  assert_frame_lists_equal(anim.effect.getKeyframes(), [
      {offset: null, computedOffset: 0, easing: 'linear', top: '100px'},
      {offset: null, computedOffset: 1, easing: 'linear', top: '200px'},
  ]);
}, 'Only properties defined directly on keyframes are considered');

test(function(t) {
  var keyframes = {};
  Object.defineProperty(keyframes, 'width', ['100px', '200px']);
  Object.defineProperty(keyframes, 'height', {
    value: ['100px', '200px'],
    enumerable: true});
  var anim = createDiv(t).animate(keyframes, 1);
  assert_frame_lists_equal(anim.effect.getKeyframes(), [
      {offset: null, computedOffset: 0, easing: 'linear', height: '100px'},
      {offset: null, computedOffset: 1, easing: 'linear', height: '200px'},
  ]);
}, 'Only enumerable properties on property indexed keyframes are considered');

test(function(t) {
  var KeyframesParent = function() { this.width = '100px'; };
  KeyframesParent.prototype = { height: '100px' };
  var Keyframes = function() { this.top = ['100px', '200px']; };
  Keyframes.prototype = Object.create(KeyframesParent.prototype);
  Object.defineProperty(Keyframes.prototype, 'left', {
    value: ['100px', '200px'],
    enumerable: 'true'});
  var keyframes = new Keyframes();
  var anim = createDiv(t).animate(keyframes, 1);
  assert_frame_lists_equal(anim.effect.getKeyframes(), [
      {offset: null, computedOffset: 0, easing: 'linear', top: '100px'},
      {offset: null, computedOffset: 1, easing: 'linear', top: '200px'},
  ]);
}, 'Only properties defined directly on property indexed keyframes are considered');

// FIXME: Test that properties are accessed in ascending order by Unicode
//        codepoint
//        (There is an existing test for this in
//        keyframe-effect/constructor.html that should be moved here.)

</script>