suite('group-animation', function() { setup(function() { document.timeline._animations = []; webAnimations1.timeline._animations = []; this.elements = []; var marginEffect = function(target) { return new KeyframeEffect( target, [ {marginLeft: '0px'}, {marginLeft: '100px'} ], 500); }; var colorEffect = function(target) { return new KeyframeEffect( target, [ {backgroundColor: 'black'}, {backgroundColor: 'white'} ], 500); }; var sequenceEmpty = function() { return new SequenceEffect(); }; var groupEmpty = function() { return new GroupEffect(); }; var sequenceWithContent = function(target) { return new SequenceEffect( [ marginEffect(target), colorEffect(target) ]); }; var groupWithContent = function(target) { return new GroupEffect( [ marginEffect(target), colorEffect(target) ]); }; var emptySeq = sequenceEmpty(); var seqSimple_target = document.createElement('div'); this.elements.push(seqSimple_target); var seqSimple = sequenceWithContent(seqSimple_target); var seqWithSeq_target = document.createElement('div'); this.elements.push(seqWithSeq_target); var seqWithSeq = new SequenceEffect( [ marginEffect(seqWithSeq_target), colorEffect(seqWithSeq_target), sequenceWithContent(seqWithSeq_target) ]); var seqWithGroup_target = document.createElement('div'); this.elements.push(seqWithGroup_target); var seqWithGroup = new SequenceEffect( [ marginEffect(seqWithGroup_target), colorEffect(seqWithGroup_target), groupWithContent(seqWithGroup_target) ]); var seqWithEmptyGroup = new SequenceEffect([groupEmpty()]); var seqWithEmptySeq = new SequenceEffect([sequenceEmpty()]); var emptyGroup = groupEmpty(); var groupSimple_target = document.createElement('div'); var groupSimple = groupWithContent(groupSimple_target); var groupWithSeq_target = document.createElement('div'); this.elements.push(groupWithSeq_target); var groutWithSeq = new GroupEffect( [ marginEffect(groupWithSeq_target), colorEffect(groupWithSeq_target), sequenceWithContent(groupWithSeq_target) ]); var groupWithGroup_target = document.createElement('div'); this.elements.push(groupWithGroup_target); var groupWithGroup = new GroupEffect( [ marginEffect(groupWithGroup_target), colorEffect(groupWithGroup_target), groupWithContent(groupWithGroup_target) ]); var groupWithEmptyGroup = new GroupEffect([groupEmpty()]); var groupWithEmptySeq = new GroupEffect([sequenceEmpty()]); this.emptySeq = emptySeq; this.seqSimple = seqSimple; this.seqWithSeq = seqWithSeq; this.seqWithGroup = seqWithGroup; this.seqWithEmptyGroup = seqWithEmptyGroup; this.seqWithEmptySeq = seqWithEmptySeq; this.emptyGroup = emptyGroup; this.groupSimple = groupSimple; this.groutWithSeq = groutWithSeq; this.groupWithGroup = groupWithGroup; this.groupWithEmptyGroup = groupWithEmptyGroup; this.groupWithEmptySeq = groupWithEmptySeq; this.staticEffect = function(target, value, duration) { var keyframeEffect = new KeyframeEffect(target, [{marginLeft: value}, {marginLeft: value}], duration); keyframeEffect.testValue = value; return keyframeEffect; }; // The following animation structure looks like: // 44444 // 11 // 33 // 2 // 0 this.complexTarget = document.createElement('div'); this.elements.push(this.complexTarget); this.complexSource = new GroupEffect([ this.staticEffect(this.complexTarget, '4px', 5), new SequenceEffect([ this.staticEffect(this.complexTarget, '1px', 2), new GroupEffect([ this.staticEffect(this.complexTarget, '3px', 2), this.staticEffect(this.complexTarget, '2px', 1), ]), ]), this.staticEffect(this.complexTarget, '0px', 1), ]); this.target = document.createElement('div'); this.target1 = document.createElement('div'); this.target2 = document.createElement('div'); this.target3 = document.createElement('div'); this.elements.push(this.target); this.elements.push(this.target1); this.elements.push(this.target2); this.elements.push(this.target3); for (var i = 0; i < this.elements.length; i++) document.documentElement.appendChild(this.elements[i]); // Playback rate test helpers. var target1 = this.target1; var target2 = this.target2; var target3 = this.target3; target1.style.transform = 'translate(500px)'; target2.style.transform = 'translate(500px)'; target3.style.transform = 'translate(500px)'; var underlyingPosition = 'matrix(1, 0, 0, 1, 500, 0)'; var startPosition = 'matrix(1, 0, 0, 1, 0, 0)'; var endPosition = 'matrix(1, 0, 0, 1, 300, 0)'; this.prChildDuration = 100; this.sequenceForPR = function(parentFill, childFill) { return new SequenceEffect([ new KeyframeEffect( target1, [{transform: 'translate(0,0)'}, {transform: 'translate(300px)'}], {duration: this.prChildDuration, fill: childFill}), new KeyframeEffect( target2, [{transform: 'translate(0,0)'}, {transform: 'translate(300px)'}], {duration: this.prChildDuration, fill: childFill}), new KeyframeEffect( target3, [{transform: 'translate(0,0)'}, {transform: 'translate(300px)'}], {duration: this.prChildDuration, fill: childFill}) ], {fill: parentFill}); }; this.isUnderlyingPosition = function() { assert.equal(getComputedStyle(target1).transform, startPosition); assert.equal(getComputedStyle(target2).transform, underlyingPosition); assert.equal(getComputedStyle(target3).transform, underlyingPosition); }; this.isFillingForwards = function() { assert.equal(getComputedStyle(target1).transform, endPosition); assert.equal(getComputedStyle(target2).transform, endPosition); }; this.isNotFillingForwards = function() { assert.equal(getComputedStyle(target1).transform, underlyingPosition); assert.equal(getComputedStyle(target2).transform, underlyingPosition); }; this.isFillingBackwardsDuring = function() { assert.equal(getComputedStyle(target2).transform, startPosition); assert.equal(getComputedStyle(target3).transform, startPosition); }; this.isNotFillingBackwardsDuring = function() { assert.equal(getComputedStyle(target2).transform, underlyingPosition); assert.equal(getComputedStyle(target3).transform, underlyingPosition); }; this.isFillingBackwards = function() { assert.equal(getComputedStyle(target1).transform, startPosition); assert.equal(getComputedStyle(target2).transform, startPosition); assert.equal(getComputedStyle(target3).transform, startPosition); }; this.isNotFillingBackwards = function() { assert.equal(getComputedStyle(target1).transform, underlyingPosition); assert.equal(getComputedStyle(target2).transform, underlyingPosition); assert.equal(getComputedStyle(target3).transform, underlyingPosition); }; this.checkFills = function(parentFillMode, childFillMode, startFill, normalFill, reverseFill, endFill, reverse) { var animation = document.timeline.play(this.sequenceForPR(parentFillMode, childFillMode)); tick(0); startFill(); tick(2 * this.prChildDuration); normalFill(); tick(this.prChildDuration * 2.5); reverse ? animation.reverse() : animation.playbackRate *= -1; tick(3.5 * this.prChildDuration); tick(5 * this.prChildDuration); reverseFill(); tick(6); tick(7.5 * this.prChildDuration); endFill(); animation.cancel(); }; }); teardown(function() { for (var i = 0; i < this.elements.length; i++) { if (this.elements[i].parent) this.elements[i].parent.removeChild(this.elements[i]); } }); function simpleGroupEffect() { return new GroupEffect([new KeyframeEffect(document.body, [], 2000), new KeyframeEffect(document.body, [], 1000), new KeyframeEffect(document.body, [], 3000)]); } function simpleSequenceEffect() { return new SequenceEffect([new KeyframeEffect(document.body, [], 2000), new KeyframeEffect(document.body, [], 1000), new KeyframeEffect(document.body, [], 3000)]); } // FIXME: Remove _startOffset. // animationState is [startTime, currentTime, _startOffset?, offset?] // innerAnimationStates is a nested array tree of animationStates e.g. [[0, 0], [[1, -1], [2, -2]]] function checkTimes(animation, animationState, innerAnimationStates, description) { description = description ? (description + ' ') : ''; _checkTimes(animation, animationState, 0, description + 'top animation'); _checkTimes(animation, innerAnimationStates, 0, description + 'inner animation'); } function _checkTimes(animation, timingList, index, trace) { assert.isDefined(animation, trace + ' exists'); if (timingList.length == 0) { assert.equal(animation._childAnimations.length, index, trace + ' no remaining animations'); return; } if (timingList[0] === null || typeof timingList[0] == 'number') { assert.equal(animation.startTime, timingList[0], trace + ' startTime'); assert.equal(animation.currentTime, timingList[1], trace + ' currentTime'); } else { _checkTimes(animation._childAnimations[index], timingList[0], 0, trace + ' ' + index); _checkTimes(animation, timingList.slice(1), index + 1, trace); } } test('playing a GroupEffect works as expected', function() { tick(90); var a = document.timeline.play(simpleGroupEffect()); checkTimes(a, [null, 0], [[null, 0], [null, 0], [null, 0]]); tick(100); checkTimes(a, [100, 0], [[100, 0], [100, 0], [100, 0]]); tick(300); checkTimes(a, [100, 200], [[100, 200], [100, 200], [100, 200]]); tick(1200); checkTimes(a, [100, 1100], [[100, 1100], [100, 1000], [100, 1100]]); tick(2200); checkTimes(a, [100, 2100], [[100, 2000], [100, 1000], [100, 2100]]); tick(3200); checkTimes(a, [100, 3000], [[100, 2000], [100, 1000], [100, 3000]]); }); test('can seek a GroupEffect', function() { tick(90); var a = document.timeline.play(simpleGroupEffect()); tick(100); checkTimes(a, [100, 0], [[100, 0], [100, 0], [100, 0]]); a.currentTime = 200; checkTimes(a, [-100, 200], [[-100, 200], [-100, 200], [-100, 200]]); a.currentTime = 1100; checkTimes(a, [-1000, 1100], [[-1000, 1100], [-1000, 1100], [-1000, 1100]]); a.currentTime = 2100; checkTimes(a, [-2000, 2100], [[-2000, 2100], [-2000, 2100], [-2000, 2100]]); a.currentTime = 3100; checkTimes(a, [-3000, 3100], [[-3000, 3100], [-3000, 3100], [-3000, 3100]]); }); test('can startTime seek a GroupEffect', function() { tick(90); var a = document.timeline.play(simpleGroupEffect()); tick(100); checkTimes(a, [100, 0], [[100, 0], [100, 0], [100, 0]]); a.startTime = -100; checkTimes(a, [-100, 200], [[-100, 200], [-100, 200], [-100, 200]]); a.startTime = -1000; checkTimes(a, [-1000, 1100], [[-1000, 1100], [-1000, 1000], [-1000, 1100]]); a.startTime = -2000; checkTimes(a, [-2000, 2100], [[-2000, 2000], [-2000, 1000], [-2000, 2100]]); a.startTime = -3000; checkTimes(a, [-3000, 3000], [[-3000, 2000], [-3000, 1000], [-3000, 3000]]); }); test('playing a SequenceEffect works as expected', function() { tick(100); var a = document.timeline.play(simpleSequenceEffect()); tick(110); checkTimes(a, [110, 0], [[110, 0], [2110, -2000], [3110, -3000]]); tick(210); checkTimes(a, [110, 100], [[110, 100], [2110, -1900], [3110, -2900]]); tick(2210); checkTimes(a, [110, 2100], [[110, 2000], [2110, 100], [3110, -900]]); tick(3210); checkTimes(a, [110, 3100], [[110, 2000], [2110, 1000], [3110, 100]]); tick(6210); checkTimes(a, [110, 6000], [[110, 2000], [2110, 1000], [3110, 3000]]); }); test('can seek a SequenceEffect', function() { tick(100); var a = document.timeline.play(simpleSequenceEffect()); tick(110); checkTimes(a, [110, 0], [[110, 0], [2110, -2000], [3110, -3000]]); a.currentTime = 100; checkTimes(a, [10, 100], [[10, 100], [2010, -1900], [3010, -2900]]); a.currentTime = 2100; checkTimes(a, [-1990, 2100], [[-1990, 2100], [10, 100], [1010, -900]]); a.currentTime = 3100; checkTimes(a, [-2990, 3100], [[-2990, 3100], [-990, 1100], [10, 100]]); a.currentTime = 6100; checkTimes(a, [-5990, 6100], [[-5990, 6100], [-3990, 4100], [-2990, 3100]]); }); test('can startTime seek a SequenceEffect', function() { tick(100); var a = document.timeline.play(simpleSequenceEffect()); tick(110); checkTimes(a, [110, 0], [[110, 0], [2110, -2000], [3110, -3000]]); a.startTime = 10; checkTimes(a, [10, 100], [[10, 100], [2010, -1900], [3010, -2900]]); a.startTime = -1990; checkTimes(a, [-1990, 2100], [[-1990, 2000], [10, 100], [1010, -900]]); a.startTime = -2990; checkTimes(a, [-2990, 3100], [[-2990, 2000], [-990, 1000], [10, 100]]); a.startTime = -5990; checkTimes(a, [-5990, 6000], [[-5990, 2000], [-3990, 1000], [-2990, 3000]]); }); test('complex animation tree timing while playing', function() { tick(90); var animation = document.timeline.play(this.complexSource); tick(100); checkTimes(animation, [100, 0], [ [100, 0], [ // 4 [100, 0], [ // 1 [102, -2], // 3 [102, -2]]], // 2 [100, 0], // 0 ], 't = 100'); tick(101); checkTimes(animation, [100, 1], [ [100, 1], [ // 4 [100, 1], [ // 1 [102, -1], // 3 [102, -1]]], // 2 [100, 1], // 0 ], 't = 101'); tick(102); checkTimes(animation, [100, 2], [ [100, 2], [ // 4 [100, 2], [ // 1 [102, 0], // 3 [102, 0]]], // 2 [100, 1], // 0 ], 't = 102'); }); test('effects apply in the correct order', function() { tick(0); var animation = document.timeline.play(this.complexSource); animation.currentTime = 0; assert.equal(getComputedStyle(this.complexTarget).marginLeft, '0px'); animation.currentTime = 1; checkTimes(animation, [-1, 1], [[-1, 1, 0], [[-1, 1, 0], [[1, -1, 0], [1, -1, 0]]], [-1, 1, 0]]); assert.equal(getComputedStyle(this.complexTarget).marginLeft, '1px'); animation.currentTime = 2; // TODO: When we seek we don't limit. Is this OK? checkTimes(animation, [-2, 2], [[-2, 2, 0], [[-2, 2, 0], [[0, 0, 0], [0, 0, 0]]], [-2, 2, 0]]); assert.equal(getComputedStyle(this.complexTarget).marginLeft, '2px'); animation.currentTime = 3; assert.equal(getComputedStyle(this.complexTarget).marginLeft, '3px'); animation.currentTime = 4; assert.equal(getComputedStyle(this.complexTarget).marginLeft, '4px'); animation.currentTime = 5; assert.equal(getComputedStyle(this.complexTarget).marginLeft, '0px'); }); test('cancelling group animations', function() { tick(0); var animation = document.timeline.play(this.complexSource); tick(1); tick(4); assert.equal(getComputedStyle(this.complexTarget).marginLeft, '3px'); animation.cancel(); assert.equal(animation.currentTime, null); assert.equal(getComputedStyle(this.complexTarget).marginLeft, '0px'); }); test('cancelling group animations before tick', function() { tick(0); var animation = document.timeline.play(this.complexSource); animation.cancel(); assert.equal(animation.currentTime, null); assert.equal(getComputedStyle(this.complexTarget).marginLeft, '0px'); tick(4); assert.equal(animation.currentTime, null); assert.equal(getComputedStyle(this.complexTarget).marginLeft, '0px'); }); test('redundant effect node wrapping', function() { tick(100); var sequenceEffect = new SequenceEffect([ this.staticEffect(this.target, '0px', 1), new GroupEffect([ new SequenceEffect([ this.staticEffect(this.target, '1px', 1), this.staticEffect(this.target, '2px', 1), ]), ]), ]); var animation = document.timeline.play(sequenceEffect); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); checkTimes(animation, [100, 0], [ [100, 0, 0, 0], [[ // 0 [101, -1, 0, 1], // 1 [102, -2, 1, 2]]] // 2 ], 't = 100'); tick(101); assert.equal(getComputedStyle(this.target).marginLeft, '1px'); checkTimes(animation, [100, 1], [ [100, 1, 0, 0], [[ // 0 [101, 0, 0, 1], // 1 [102, -1, 1, 2]]] // 2 ], 't = 101'); tick(102); assert.equal(getComputedStyle(this.target).marginLeft, '2px'); assert.equal(document.timeline.currentTime, 102); checkTimes(animation, [100, 2], [ // FIXME: Implement limiting on group animations [100, 1, 0, 0], [[ // 0 [101, 1, 0, 1], // 1 [102, 0, 1, 2]]] // 2 ], 't = 102'); tick(103); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); checkTimes(animation, [100, 3], [ // FIXME: Implement limiting on group animations [100, 1, 0, 0], [[ // 0 [101, 1, 0, 1], // 1 [102, 1, 1, 2]]] // 2 ], 't = 103'); if (this.target.parent) this.target.parent.removeChild(target); }); test('Fill modes work for sequence fill both with children none after setting playbackRate from positive to negative.', function() { this.checkFills( 'both', 'none', this.isUnderlyingPosition, this.isNotFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, false ); }); test('Fill modes work for sequence fill both with children both after setting playbackRate from positive to negative.', function() { this.checkFills( 'both', 'both', this.isFillingBackwards, this.isFillingForwards, this.isFillingBackwardsDuring, this.isFillingBackwards, false ); }); test('Fill modes work for sequence fill both with children backwards after setting playbackRate from positive to negative.', function() { this.checkFills( 'both', 'backwards', this.isFillingBackwards, this.isNotFillingForwards, this.isFillingBackwardsDuring, this.isFillingBackwards, false ); }); test('Fill modes work for sequence fill both with children forwards after setting playbackRate from positive to negative.', function() { this.checkFills( 'both', 'forwards', this.isUnderlyingPosition, this.isFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, false ); }); test('Fill modes work for sequence fill none with children fill none after setting playbackRate from positive to negative.', function() { this.checkFills( 'none', 'none', this.isUnderlyingPosition, this.isNotFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, false ); }); test('Fill modes work for sequence fill none with children fill both after setting playbackRate from positive to negative.', function() { this.checkFills( 'none', 'both', this.isFillingBackwards, this.isFillingForwards, this.isFillingBackwardsDuring, this.isNotFillingBackwards, false ); }); test('Fill modes work for sequence fill none with children fill backwards after setting playbackRate from positive to negative.', function() { this.checkFills( 'none', 'backwards', this.isFillingBackwards, this.isNotFillingForwards, this.isFillingBackwardsDuring, this.isNotFillingBackwards, false ); }); test('Fill modes work for sequence fill none with children fill forwards after setting playbackRate from positive to negative.', function() { this.checkFills( 'none', 'forwards', this.isUnderlyingPosition, this.isFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, false ); }); test('Fill modes work for sequence fill both with children none after reverse.', function() { this.checkFills( 'both', 'none', this.isUnderlyingPosition, this.isNotFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, true ); }); test('Fill modes work for sequence fill both with children both after reverse.', function() { this.checkFills( 'both', 'both', this.isFillingBackwards, this.isFillingForwards, this.isFillingBackwardsDuring, this.isFillingBackwards, true ); }); test('Fill modes work for sequence fill both with children backwards after reverse.', function() { this.checkFills( 'both', 'backwards', this.isFillingBackwards, this.isNotFillingForwards, this.isFillingBackwardsDuring, this.isFillingBackwards, true ); }); test('Fill modes work for sequence fill both with children forwards after reverse.', function() { this.checkFills( 'both', 'forwards', this.isUnderlyingPosition, this.isFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, true ); }); test('Fill modes work for sequence fill none with children fill none after reverse.', function() { this.checkFills( 'none', 'none', this.isUnderlyingPosition, this.isNotFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, true ); }); test('Fill modes work for sequence fill none with children fill both after reverse.', function() { this.checkFills( 'none', 'both', this.isFillingBackwards, this.isFillingForwards, this.isFillingBackwardsDuring, this.isNotFillingBackwards, true ); }); test('Fill modes work for sequence fill none with children fill backwards after reverse.', function() { this.checkFills( 'none', 'backwards', this.isFillingBackwards, this.isNotFillingForwards, this.isFillingBackwardsDuring, this.isNotFillingBackwards, true ); }); test('Fill modes work for sequence fill none with children fill forwards after reverse.', function() { this.checkFills( 'none', 'forwards', this.isUnderlyingPosition, this.isFillingForwards, this.isNotFillingBackwardsDuring, this.isNotFillingBackwards, true ); }); test('Setting the playbackRate on sequence animations updates child timing. ' + 'Any children who are not finished go into effect.', function() { var sequenceEffect = new SequenceEffect([ new KeyframeEffect(null, [], 1000), new KeyframeEffect(null, [], 1000), ]); var a = document.timeline.play(sequenceEffect); tick(0); a.playbackRate = 2; assert.equal(a._animation.playbackRate, 2, 'Updates the playbackRate of the inner animation'); a._childAnimations.forEach(function(childAnimation) { assert.equal(childAnimation.playbackRate, 2, 'It also updates the child animations'); }); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, -1000); assert.equal(a.startTime, null); assert.equal(a._childAnimations[0].startTime, null); assert.equal(a._childAnimations[1].startTime, null); tick(1); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, -1000); assert.equal(a.startTime, 1); assert.equal(a._childAnimations[0].startTime, 1); assert.equal(a._childAnimations[1].startTime, 501); tick(601); assert.equal(a.currentTime, 1200); assert.equal(a._childAnimations[0].currentTime, 1000); assert.equal(a._childAnimations[1].currentTime, 200); assert.equal(a.startTime, 1); assert.equal(a._childAnimations[0].startTime, 1); assert.equal(a._childAnimations[1].startTime, 501); tick(1101); assert.equal(a.currentTime, 2000); assert.equal(a._childAnimations[0].currentTime, 1000); assert.equal(a._childAnimations[1].currentTime, 1000); assert.equal(a.startTime, 1); assert.equal(a._childAnimations[0].startTime, 1); assert.equal(a._childAnimations[1].startTime, 501); a.playbackRate = -1; assert.equal(a._animation.playbackRate, -1, 'Updates the playbackRate of the inner animation'); a._childAnimations.forEach(function(childAnimation) { assert.equal(childAnimation.playbackRate, -1, 'It also updates the child animations'); }); assert.equal(a.currentTime, 2000); assert.equal(a._childAnimations[0].currentTime, 2000); assert.equal(a._childAnimations[1].currentTime, 1000); assert.equal(a.startTime, null); assert.equal(a._childAnimations[0].startTime, null); assert.equal(a._childAnimations[1].startTime, null); tick(1102); assert.equal(a.currentTime, 2000); assert.equal(a._childAnimations[0].currentTime, 2000); assert.equal(a._childAnimations[1].currentTime, 1000); assert.equal(a.startTime, 3102); assert.equal(a._childAnimations[0].startTime, 3102); assert.equal(a._childAnimations[1].startTime, 2102); tick(1602); assert.equal(a.currentTime, 1500); assert.equal(a._childAnimations[0].currentTime, 1500); assert.equal(a._childAnimations[1].currentTime, 500); assert.equal(a.startTime, 3102); assert.equal(a._childAnimations[0].startTime, 3102); assert.equal(a._childAnimations[1].startTime, 2102); tick(3103); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, 0); assert.equal(a.startTime, 3102); assert.equal(a._childAnimations[0].startTime, 3102); assert.equal(a._childAnimations[1].startTime, 2102); a.playbackRate = 1; assert.equal(a._animation.playbackRate, 1, 'Updates the playbackRate of the inner animation'); a._childAnimations.forEach(function(childAnimation) { assert.equal(childAnimation.playbackRate, 1, 'It also updates the child animations'); }); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, -1000); assert.equal(a.startTime, null); assert.equal(a._childAnimations[0].startTime, null); assert.equal(a._childAnimations[1].startTime, null); tick(3104); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, -1000); assert.equal(a.startTime, 3104); assert.equal(a._childAnimations[0].startTime, 3104); assert.equal(a._childAnimations[1].startTime, 4104); tick(3604); assert.equal(a.currentTime, 500); assert.equal(a._childAnimations[0].currentTime, 500); assert.equal(a._childAnimations[1].currentTime, -500); assert.equal(a.startTime, 3104); assert.equal(a._childAnimations[0].startTime, 3104); assert.equal(a._childAnimations[1].startTime, 4104); } ); test('Reversing a sequence animation updates child timing correctly', function() { var sequenceEffect = new SequenceEffect([ new KeyframeEffect(null, [], 1000), new KeyframeEffect(null, [], 1000), ]); var a = document.timeline.play(sequenceEffect); tick(0); a.playbackRate = 2; assert.equal(a._animation.playbackRate, 2, 'Updates the playbackRate of the inner animation'); a._childAnimations.forEach(function(childAnimation) { assert.equal(childAnimation.playbackRate, 2, 'It also updates the child animations'); }); tick(1); tick(1101); assert.equal(a.currentTime, 2000); assert.equal(a._childAnimations[0].currentTime, 1000); assert.equal(a._childAnimations[1].currentTime, 1000); assert.equal(a.startTime, 1); assert.equal(a._childAnimations[0].startTime, 1); assert.equal(a._childAnimations[1].startTime, 501); a.reverse(); assert.equal(a._animation.playbackRate, -2, 'Updates the playbackRate of the inner animation'); a._childAnimations.forEach(function(childAnimation) { assert.equal(childAnimation.playbackRate, -2, 'It also updates the child animations'); }); assert.equal(a.currentTime, 2000); assert.equal(a._childAnimations[0].currentTime, 2000); assert.equal(a._childAnimations[1].currentTime, 1000); assert.equal(a.startTime, null); assert.equal(a._childAnimations[0].startTime, null); assert.equal(a._childAnimations[1].startTime, null); tick(1102); assert.equal(a.currentTime, 2000); assert.equal(a._childAnimations[0].currentTime, 2000); assert.equal(a._childAnimations[1].currentTime, 1000); assert.equal(a.startTime, 2102); assert.equal(a._childAnimations[0].startTime, 2102); assert.equal(a._childAnimations[1].startTime, 1602); tick(1602); assert.equal(a.currentTime, 1000); assert.equal(a._childAnimations[0].currentTime, 1000); assert.equal(a._childAnimations[1].currentTime, 0); assert.equal(a.startTime, 2102); assert.equal(a._childAnimations[0].startTime, 2102); assert.equal(a._childAnimations[1].startTime, 1602); tick(3103); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, 0); assert.equal(a.startTime, 2102); assert.equal(a._childAnimations[0].startTime, 2102); assert.equal(a._childAnimations[1].startTime, 1602); a.reverse(); assert.equal(a._animation.playbackRate, 2, 'Updates the playbackRate of the inner animation'); a._childAnimations.forEach(function(childAnimation) { assert.equal(childAnimation.playbackRate, 2, 'It also updates the child animations'); }); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, -1000); assert.equal(a.startTime, null); assert.equal(a._childAnimations[0].startTime, null); assert.equal(a._childAnimations[1].startTime, null); tick(3104); assert.equal(a.currentTime, 0); assert.equal(a._childAnimations[0].currentTime, 0); assert.equal(a._childAnimations[1].currentTime, -1000); assert.equal(a.startTime, 3104); assert.equal(a._childAnimations[0].startTime, 3104); assert.equal(a._childAnimations[1].startTime, 3604); tick(3604); assert.equal(a.currentTime, 1000); assert.equal(a._childAnimations[0].currentTime, 1000); assert.equal(a._childAnimations[1].currentTime, 0); assert.equal(a.startTime, 3104); assert.equal(a._childAnimations[0].startTime, 3104); assert.equal(a._childAnimations[1].startTime, 3604); }); test('delays on groups work correctly', function() { // 444 // 1 // 0 // 33 // 2 var groupEffect = new GroupEffect([ new GroupEffect([ this.staticEffect(this.target, '4px', {duration: 3, delay: 1}), this.staticEffect(this.target, '1px', {duration: 1, delay: 0}), ], {delay: 1}), new SequenceEffect([ this.staticEffect(this.target, '0px', {duration: 1, delay: 0}), this.staticEffect(this.target, '3px', {duration: 2, delay: 1}), this.staticEffect(this.target, '2px', {duration: 1, delay: -2}), ]), ]); var animation = document.timeline.play(groupEffect); tick(100); checkTimes(animation, [100, 0], [ [ [101, -1], [101, -1], ], [ [100, 0], [101, -1], [104, -4], ] ]); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); tick(101); assert.equal(getComputedStyle(this.target).marginLeft, '1px'); tick(102); assert.equal(getComputedStyle(this.target).marginLeft, '2px'); tick(103); assert.equal(getComputedStyle(this.target).marginLeft, '3px'); tick(104); assert.equal(getComputedStyle(this.target).marginLeft, '4px'); tick(105); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); }); test('end delays on groups work correctly', function() { // 11 // 4 // 0 // 33 // 2 var sequenceEffect = new SequenceEffect([ new SequenceEffect([ this.staticEffect(this.target, '1px', {duration: 2, endDelay: 2}), this.staticEffect(this.target, '4px', {duration: 1, endDelay: 1}), ], {endDelay: -6}), new SequenceEffect([ this.staticEffect(this.target, '0px', {duration: 1, endDelay: 1}), this.staticEffect(this.target, '3px', {duration: 2, endDelay: -2}), this.staticEffect(this.target, '2px', {duration: 1, endDelay: 2}), ]), ]); var animation = document.timeline.play(sequenceEffect); tick(100); checkTimes(animation, [100, 0], [ [ [100, 0], [104, -4], ], [ [100, 0], [102, -2], [102, -2], ] ]); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); tick(101); assert.equal(getComputedStyle(this.target).marginLeft, '1px'); tick(102); assert.equal(getComputedStyle(this.target).marginLeft, '2px'); tick(103); assert.equal(getComputedStyle(this.target).marginLeft, '3px'); tick(104); // FIXME: Group child animation limiting bounds should match the parent animation's limiting bounds. // assert.equal(getComputedStyle(this.target).marginLeft, '4px'); // tick(105); // assert.equal(getComputedStyle(this.target).marginLeft, '0px'); }); test('basic animation operations are working', function() { var animations = []; animations.push(document.timeline.play(this.emptySeq)); animations.push(document.timeline.play(this.seqSimple)); animations.push(document.timeline.play(this.seqWithSeq)); animations.push(document.timeline.play(this.seqWithGroup)); animations.push(document.timeline.play(this.seqWithEmptyGroup)); animations.push(document.timeline.play(this.seqWithEmptySeq)); animations.push(document.timeline.play(this.emptyGroup)); animations.push(document.timeline.play(this.groupSimple)); animations.push(document.timeline.play(this.groutWithSeq)); animations.push(document.timeline.play(this.groupWithGroup)); animations.push(document.timeline.play(this.groupWithEmptyGroup)); animations.push(document.timeline.play(this.groupWithEmptySeq)); var length = animations.length; tick(50); for (var i = 0; i < length; i++) animations[i].pause(); tick(100); for (var i = 0; i < length; i++) animations[i].play(); tick(200); for (var i = 0; i < length; i++) animations[i].currentTime += 1; tick(300); for (var i = 0; i < length; i++) animations[i].startTime += 1; tick(350); for (var i = 0; i < length; i++) animations[i].reverse(); tick(400); for (var i = 0; i < length; i++) animations[i].finish(); tick(500); tick(600); for (var i = 0; i < length; i++) animations[i].cancel(); for (var i = 0; i < length; i++) animations[i].play(); }); test('pausing works as expected with an empty SequenceEffect', function() { var animation = document.timeline.play(this.emptySeq); tick(0); assert.equal(animation.startTime, 0); assert.equal(animation.currentTime, 0); animation.pause(); assert.equal(animation.startTime, null); assert.equal(animation.currentTime, 0); }); test('pausing works as expected with a simple SequenceEffect', function() { var animation = document.timeline.play(this.seqSimple); var target = this.seqSimple.children[0].target; tick(0); checkTimes(animation, [0, 0], [[0, 0], [500, -500]], 't = 0'); tick(200); checkTimes(animation, [0, 200], [[0, 200], [500, -300]], 't = 200'); animation.pause(); checkTimes(animation, [null, null], [[null, null], [null, null]], 't = 200'); assert.equal(getComputedStyle(target).marginLeft, '40px'); tick(300); checkTimes(animation, [null, 200], [[null, 200], [null, -300]], 't = 300'); assert.equal(getComputedStyle(target).marginLeft, '40px'); animation.play(); checkTimes(animation, [null, 200], [[null, 200], [null, -300]], 't = 300'); assert.equal(getComputedStyle(target).marginLeft, '40px'); tick(301); checkTimes(animation, [101, 200], [[101, 200], [601, -300]], 't = 301'); assert.equal(getComputedStyle(target).marginLeft, '40px'); tick(401); checkTimes(animation, [101, 300], [[101, 300], [601, -200]], 't = 401'); assert.equal(getComputedStyle(target).marginLeft, '60px'); tick(700); checkTimes(animation, [101, 599], [[101, 500], [601, 99]], 't = 700'); assert.equal(getComputedStyle(target).marginLeft, '0px'); }); test('pausing before tick works as expected with a simple SequenceEffect', function() { var animation = document.timeline.play(this.seqSimple); var target = this.seqSimple.children[0].target; checkTimes(animation, [null, 0], [[null, 0], [null, -500]], 't = 0'); animation.pause(); checkTimes(animation, [null, null], [[null, null], [null, null]], 't = 0'); assert.equal(getComputedStyle(target).marginLeft, '0px'); tick(10); checkTimes(animation, [null, 0], [[null, 0], [null, -500]], 't = 10'); assert.equal(getComputedStyle(target).marginLeft, '0px'); tick(20); checkTimes(animation, [null, 0], [[null, 0], [null, -500]], 't = 10'); assert.equal(getComputedStyle(target).marginLeft, '0px'); }); test('pausing and seeking before tick works as expected with a simple SequenceEffect', function() { var animation = document.timeline.play(this.seqSimple); animation.pause(); animation.currentTime = 0; checkTimes(animation, [null, 0], [[null, 0], [null, -500]], 't = 10'); animation.currentTime = 250; checkTimes(animation, [null, 250], [[null, 250], [null, -250]], 't = 10'); animation.currentTime = 500; checkTimes(animation, [null, 500], [[null, 500], [null, 0]], 't = 10'); // FIXME: Expectation should be [null, 1000], [[null, 500], [null, 500]]. animation.currentTime = 1000; checkTimes(animation, [null, 1000], [[null, 1000], [null, 500]], 't = 10'); }); test('pausing works as expected with an SequenceEffect inside an SequenceEffect', function() { var animation = document.timeline.play(this.seqWithSeq); tick(0); checkTimes( animation, [0, 0], [ [0, 0], [500, -500], [ [1000, -1000], [1500, -1500]]], 't = 0'); tick(200); checkTimes( animation, [0, 200], [ [0, 200], [500, -300], [ [1000, -800], [1500, -1300]]], 't = 200'); animation.pause(); checkTimes( animation, [null, null], [ [null, null], [null, null], [ [null, null], [null, null]]], 't = 200'); tick(300); checkTimes( animation, [null, 200], [ [null, 200], [null, -300], [ [null, -800], [null, -1300]]], 't = 300'); animation.play(); tick(310); checkTimes( animation, [110, 200], [ [110, 200], [610, -300], [ [1110, -800], [1610, -1300]]], 't = 310'); tick(1300); checkTimes( animation, [110, 1190], [ [110, 500], [610, 500], [ [1110, 190], [1610, -310]]], 't = 1300'); animation.pause(); checkTimes( animation, [null, null], [ [null, 500], [null, 500], [ [null, null], [null, null]]], 't = 1300'); tick(1400); checkTimes( animation, [null, 1190], [ [null, 500], [null, 500], [ [null, 190], [null, -310]]], 't = 1400'); animation.play(); checkTimes( animation, [null, 1190], [ [null, 500], [null, 500], [ [null, 190], [null, -310]]], 't = 1400'); tick(1410); checkTimes( animation, [220, 1190], [ [220, 500], [720, 500], [ [1220, 190], [1720, -310]]], 't = 1410'); tick(1600); checkTimes( animation, [220, 1380], [ [220, 500], [720, 500], [ [1220, 380], [1720, -120]]], 't = 1600'); animation.pause(); checkTimes( animation, [null, null], [ [null, 500], [null, 500], [ [null, null], [null, null]]], 't = 1600'); tick(1700); checkTimes( animation, [null, 1380], [ [null, 500], [null, 500], [ [null, 380], [null, -120]]], 't = 1700'); animation.play(); tick(1710); checkTimes( animation, [330, 1380], [ [330, 500], [830, 500], [ [1330, 380], [1830, -120]]], 't = 1710'); tick(2400); checkTimes( animation, [330, 2000], [ [330, 500], [830, 500], [ [1330, 500], [1830, 500]]], 't = 2400'); }); test('pausing works as expected with a GroupEffect inside an SequenceEffect', function() { var animation = document.timeline.play(this.seqWithGroup); tick(0); checkTimes( animation, [0, 0], [ [0, 0], [500, -500], [ [1000, -1000], [1000, -1000]]], 't = 0'); tick(200); checkTimes( animation, [0, 200], [ [0, 200], [500, -300], [ [1000, -800], [1000, -800]]], 't = 200'); animation.pause(); checkTimes( animation, [null, null], [ [null, null], [null, null], [ [null, null], [null, null]]], 't = 200'); tick(300); checkTimes( animation, [null, 200], [ [null, 200], [null, -300], [ [null, -800], [null, -800]]], 't = 300'); animation.play(); tick(310); checkTimes( animation, [110, 200], [ [110, 200], [610, -300], [ [1110, -800], [1110, -800]]], 't = 310'); tick(1310); checkTimes( animation, [110, 1200], [ [110, 500], [610, 500], [ [1110, 200], [1110, 200]]], 't = 1310'); animation.pause(); checkTimes( animation, [null, null], [ [null, 500], [null, 500], [ [null, null], [null, null]]], 't = 1310'); tick(1400); checkTimes( animation, [null, 1200], [ [null, 500], [null, 500], [ [null, 200], [null, 200]]], 't = 1410'); animation.play(); tick(1410); checkTimes( animation, [210, 1200], [ [210, 500], [710, 500], [ [1210, 200], [1210, 200]]], 't = 1410'); tick(1610); checkTimes( animation, [210, 1400], [ [210, 500], [710, 500], [ [1210, 400], [1210, 400]]], 't = 1610'); animation.pause(); tick(1810); checkTimes( animation, [null, 1400], [ [null, 500], [null, 500], [ [null, 400], [null, 400]]], 't = 1810'); animation.play(); tick(1820); checkTimes( animation, [420, 1400], [ [420, 500], [920, 500], [ [1420, 400], [1420, 400]]], 't = 1820'); tick(2020); checkTimes( animation, [420, 1500], [ [420, 500], [920, 500], [ [1420, 500], [1420, 500]]], 't = 2020'); animation.pause(); checkTimes( animation, [null, 1500], [ [null, 500], [null, 500], [ [null, 500], [null, 500]]], 't = 2020'); }); test('pausing works as expected with an empty SequenceEffect inside an SequenceEffect', function() { var animation = document.timeline.play(this.seqWithEmptySeq); tick(0); checkTimes( animation, [0, 0], [0, 0], 't = 0'); animation.pause(); checkTimes( animation, [null, 0], [null, 0], 't = 0 after pause'); }); test('pausing works as expected with an empty GroupEffect inside an SequenceEffect', function() { var animation = document.timeline.play(this.seqWithEmptyGroup); tick(0); checkTimes( animation, [0, 0], [0, 0], 't = 0'); animation.pause(); checkTimes( animation, [null, 0], [null, 0], 't = 0 after pause'); }); test('playState works for groups', function() { var target = document.createElement('div'); document.body.appendChild(target); var sequenceEffect = new SequenceEffect([new KeyframeEffect(target, [], 100), new KeyframeEffect(target, [], 100)]); var a = document.timeline.play(sequenceEffect); assert.equal(a.playState, 'pending'); tick(1); assert.equal(a.playState, 'running'); assert.equal(a._childAnimations[0]._animation.playState, 'running'); assert.equal(a._childAnimations[1]._animation.playState, 'running'); tick(101); assert.equal(a.playState, 'running'); assert.equal(a._childAnimations[0]._animation.playState, 'finished'); assert.equal(a._childAnimations[1]._animation.playState, 'running'); a.pause(); assert.equal(a.playState, 'pending'); assert.equal(a._childAnimations[0]._animation.playState, 'paused'); assert.equal(a._childAnimations[1]._animation.playState, 'pending'); tick(102); assert.equal(a.playState, 'paused'); assert.equal(a._childAnimations[0]._animation.playState, 'paused'); assert.equal(a._childAnimations[1]._animation.playState, 'paused'); a.play(); assert.equal(a.playState, 'pending'); assert.equal(a._childAnimations[0]._animation.playState, 'pending'); assert.equal(a._childAnimations[1]._animation.playState, 'pending'); tick(103); assert.equal(a.playState, 'running'); assert.equal(a._childAnimations[0]._animation.playState, 'finished'); assert.equal(a._childAnimations[1]._animation.playState, 'running'); tick(204); assert.equal(a.playState, 'finished'); assert.equal(a._childAnimations[0]._animation.playState, 'finished'); assert.equal(a._childAnimations[1]._animation.playState, 'finished'); }); test('pausing then seeking out of range then seeking into range works', function() { var target = document.createElement('div'); var keyframeEffect = new KeyframeEffect(target, [], {duration: 2000, fill: 'both'}); var groupEffect = new GroupEffect([keyframeEffect], {fill: 'none'}); var animation = document.timeline.play(groupEffect); animation.pause(); animation.currentTime = 3000; assert.equal(animation._childAnimations.length, 0); tick(100); animation.currentTime = 1000; assert.equal(animation._childAnimations.length, 1); assert.equal(animation._childAnimations[0]._animation.playState, 'paused'); assert.equal(animation._childAnimations[0]._animation.currentTime, 1000); }); test('reversing then seeking out of range then seeking into range works', function() { var target = document.createElement('div'); var keyframeEffect = new KeyframeEffect(target, [], {duration: 2000, fill: 'both'}); var groupEffect = new GroupEffect([keyframeEffect], {fill: 'none'}); var animation = document.timeline.play(groupEffect); animation.currentTime = 1000; tick(100); animation.reverse(); tick(105); animation.currentTime = 3000; assert.equal(animation._childAnimations.length, 0); tick(110); animation.currentTime = 1000; assert.equal(animation.playbackRate, -1); assert.equal(animation._childAnimations.length, 1); assert.equal(animation._childAnimations[0]._animation.playState, 'running'); assert.equal(animation._childAnimations[0]._animation.currentTime, 1000); assert.equal(animation._childAnimations[0]._animation.playbackRate, -1); }); test('fill none groups with fill none children do not fill', function() { var keyframeEffect = new KeyframeEffect( this.target, [{marginLeft: '0px'}, {marginLeft: '100px'}], {duration: 500, fill: 'none'}); var groupEffect = new GroupEffect([keyframeEffect], {fill: 'none'}); var animation = document.timeline.play(groupEffect); tick(0); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); tick(250); assert.equal(getComputedStyle(this.target).marginLeft, '50px'); tick(501); assert.equal(getComputedStyle(this.target).marginLeft, '0px'); tick(502); }); });