describe 'up.motion', -> u = up.util describe 'JavaScript functions', -> describe 'up.animate', -> describeCapability 'canCssTransition', -> it 'animates the given element', (done) -> $element = affix('.element').text('content') up.animate($element, 'fade-in', duration: 200, easing: 'linear') u.setTimer 0, -> expect(u.opacity($element)).toBeAround(0.0, 0.25) u.setTimer 100, -> expect(u.opacity($element)).toBeAround(0.5, 0.25) u.setTimer 200, -> expect(u.opacity($element)).toBeAround(1.0, 0.25) done() it 'returns a promise that is resolved when the animation completed', (done) -> $element = affix('.element').text('content') resolveSpy = jasmine.createSpy('resolve') promise = up.animate($element, 'fade-in', duration: 100, easing: 'linear') promise.then(resolveSpy) u.setTimer 50, -> expect(resolveSpy).not.toHaveBeenCalled() u.setTimer 100, -> expect(resolveSpy).toHaveBeenCalled() done() it 'cancels an existing animation on the element by instantly jumping to the last frame', -> $element = affix('.element').text('content') up.animate($element, { 'font-size': '40px' }, duration: 10000, easing: 'linear') up.animate($element, { 'fade-in' }, duration: 100, easing: 'linear') expect($element.css('font-size')).toEqual('40px') describe 'with animations disabled globally', -> beforeEach -> up.motion.config.enabled = false it "doesn't animate and directly sets the last frame instead", (done) -> $element = affix('.element').text('content') callback = jasmine.createSpy('animation done callback') animateDone = up.animate($element, { 'font-size': '40px' }, duration: 10000, easing: 'linear') animateDone.then(callback) expect($element.css('font-size')).toEqual('40px') u.nextFrame -> expect(callback).toHaveBeenCalled() done() [false, null, undefined, 'none', up.motion.none()].forEach (noneAnimation) -> describe "when called with a `#{noneAnimation}` animation", -> it "doesn't animate and resolves instantly", (done) -> $element = affix('.element').text('content') callback = jasmine.createSpy('animation done callback') animateDone = up.animate($element, noneAnimation, duration: 10000, easing: 'linear') animateDone.then(callback) u.nextFrame -> expect(callback).toHaveBeenCalled() done() describeFallback 'canCssTransition', -> it "doesn't animate and directly sets the last frame instead", -> $element = affix('.element').text('content') up.animate($element, { 'font-size': '40px' }, duration: 10000, easing: 'linear') expect($element.css('font-size')).toEqual('40px') describe 'up.motion.finish', -> describe 'when called with an element or selector', -> describeCapability 'canCssTransition', -> it 'cancels an existing animation on the given element by instantly jumping to the last frame', -> $element = affix('.element').text('content') up.animate($element, { 'font-size': '40px', 'opacity': '0.33' }, duration: 10000) up.motion.finish($element) expect($element.css('font-size')).toEqual('40px') expect($element.css('opacity')).toEqual('0.33') it 'cancels animations on children of the given element', -> $parent = affix('.element') $child = $parent.affix('.child') up.animate($child, { 'font-size': '40px' }, duration: 10000) up.motion.finish($parent) expect($child.css('font-size')).toEqual('40px') it 'does not cancel animations on other elements', -> $element1 = affix('.element1').text('content1') $element2 = affix('.element2').text('content2') up.animate($element1, 'fade-in', duration: 10000) up.animate($element2, 'fade-in', duration: 10000) up.motion.finish($element1) expect(Number($element1.css('opacity'))).toEqual(1) expect(Number($element2.css('opacity'))).toEqual(0, 0.1) it 'restores existing transitions on the element', -> $element = affix('.element').text('content') $element.css('transition': 'font-size 3s ease') oldTransitionProperty = $element.css('transition-property') expect(oldTransitionProperty).toBeDefined() expect(oldTransitionProperty).toContain('font-size') # be paranoid up.animate($element, 'fade-in', duration: 10000) up.motion.finish($element) expect(u.opacity($element)).toEqual(1) currentTransitionProperty = $element.css('transition-property') expect(currentTransitionProperty).toEqual(oldTransitionProperty) expect(currentTransitionProperty).toContain('font-size') expect(currentTransitionProperty).not.toContain('opacity') it 'cancels an existing transition on the element by instantly jumping to the last frame', -> $old = affix('.old').text('old content') $new = affix('.new').text('new content') up.morph($old, $new, 'cross-fade', duration: 2000) expect($('.up-ghost').length).toBe(2) up.motion.finish($old) expect($('.up-ghost').length).toBe(0) expect($old.css('display')).toEqual('none') expect($new.css('display')).toEqual('block') it 'can be called on either element involved in a transition', -> $old = affix('.old').text('old content') $new = affix('.new').text('new content') up.morph($old, $new, 'cross-fade', duration: 2000) expect($('.up-ghost').length).toBe(2) up.motion.finish($new) expect($('.up-ghost').length).toBe(0) expect($old.css('display')).toEqual('none') expect($new.css('display')).toEqual('block') it 'cancels transitions on children of the given element', -> $parent = affix('.parent') $old = $parent.affix('.old').text('old content') $new = $parent.affix('.new').text('new content') up.morph($old, $new, 'cross-fade', duration: 2000) expect($('.up-ghost').length).toBe(2) up.motion.finish($parent) expect($('.up-ghost').length).toBe(0) expect($old.css('display')).toEqual('none') expect($new.css('display')).toEqual('block') describeFallback 'canCssTransition', -> it 'does nothing' describe 'when called without arguments', -> describeCapability 'canCssTransition', -> it 'cancels all animations on the screen', -> $element1 = affix('.element1').text('content1') $element2 = affix('.element2').text('content2') up.animate($element1, 'fade-in', duration: 3000) up.animate($element2, 'fade-in', duration: 3000) expect(u.opacity($element1)).toBeAround(0.0, 0.1) expect(u.opacity($element2)).toBeAround(0.0, 0.1) up.motion.finish() $element1 = $('.element1') $element2 = $('.element2') expect(u.opacity($element1)).toBe(1.0) expect(u.opacity($element2)).toBe(1.0) describeFallback 'canCssTransition', -> it 'does nothing' describe 'up.morph', -> describeCapability 'canCssTransition', -> it 'transitions between two element by animating two copies while keeping the originals in the background', (done) -> $old = affix('.old').text('old content').css( position: 'absolute' top: '10px' left: '11px', width: '12px', height: '13px' ) $new = affix('.new').text('new content').css( position: 'absolute' top: '20px' left: '21px', width: '22px', height: '23px' ) up.morph($old, $new, 'cross-fade', duration: 200, easing: 'linear') # The actual animation will be performed on Ghosts since # two element usually cannot exist in the DOM at the same time # without undesired visual effects $oldGhost = $('.old.up-ghost') $newGhost = $('.new.up-ghost') expect($oldGhost).toExist() expect($newGhost).toExist() $oldBounds = $oldGhost.parent('.up-bounds') $newBounds = $newGhost.parent('.up-bounds') expect($oldBounds).toExist() expect($newBounds).toExist() # Ghosts should be inserted before (not after) the element # or the browser scroll position will be too low after the # transition ends. expect($oldGhost.parent().next()).toEqual($old) expect($newGhost.parent().next()).toEqual($new) # The old element is removed from the layout flow. # It will be removed from the DOM after the animation has ended. expect($old.css('display')).toEqual('none') # The new element is invisible due to an opacity of zero, # but takes up the space in the layout flow. expect($new.css(['display', 'opacity'])).toEqual( display: 'block', opacity: '0' ) # We **must not** use `visibility: hidden` to hide the new # element. This would delay browser painting until the element is # shown again, causing a flicker while the browser is painting. expect($new.css('visibility')).not.toEqual('hidden') # Ghosts will hover over $old and $new using absolute positioning, # matching the coordinates of the original elements. expect($oldBounds.css(['position', 'top', 'left', 'width', 'height'])).toEqual( position: 'absolute' top: '10px' left: '11px', width: '12px', height: '13px' ) expect($newBounds.css(['position', 'top', 'left', 'width', 'height'])).toEqual( position: 'absolute' top: '20px' left: '21px', width: '22px', height: '23px' ) u.setTimer 0, -> expect(u.opacity($newGhost)).toBeAround(0.0, 0.25) expect(u.opacity($oldGhost)).toBeAround(1.0, 0.25) u.setTimer 80, -> expect(u.opacity($newGhost)).toBeAround(0.4, 0.25) expect(u.opacity($oldGhost)).toBeAround(0.6, 0.25) u.setTimer 140, -> expect(u.opacity($newGhost)).toBeAround(0.7, 0.25) expect(u.opacity($oldGhost)).toBeAround(0.3, 0.25) u.setTimer 250, -> # Once our two ghosts have rendered their visual effect, # we remove them from the DOM. expect($newGhost).not.toBeInDOM() expect($oldGhost).not.toBeInDOM() # The old element is still in the DOM, but hidden. # Morphing does *not* remove the target element. expect($old.css('display')).toEqual('none') expect($new.css(['display', 'visibility'])).toEqual( display: 'block', visibility: 'visible' ) done() it 'cancels an existing transition on the element by instantly jumping to the last frame', -> $old = affix('.old').text('old content') $new = affix('.new').text('new content') up.morph($old, $new, 'cross-fade', duration: 200) $ghost1 = $('.old.up-ghost') expect($ghost1).toHaveLength(1) up.morph($old, $new, 'cross-fade', duration: 200) $ghost2 = $('.old.up-ghost') # Check that we didn't create additional ghosts expect($ghost2).toHaveLength(1) # Check that it's a different ghosts expect($ghost2).not.toEqual($ghost1) describe 'with { reveal: true } option', -> it 'reveals the new element while making the old element within the same viewport appear as if it would keep its scroll position', -> $container = affix('.container[up-viewport]').css 'width': '200px' 'height': '200px' 'overflow-y': 'scroll' 'position': 'fixed' 'left': 0, 'top': 0 $old = affix('.old').appendTo($container).css(height: '600px') $container.scrollTop(300) $new = affix('.new').insertBefore($old).css(height: '600px') up.morph($old, $new, 'cross-fade', duration: 50, reveal: true) $oldGhost = $('.old.up-ghost') $newGhost = $('.new.up-ghost') # Container is scrolled up due to { reveal: true } option. # Since $old and $new are sitting in the same viewport with a # single shares scrollbar This will make the ghost for $old jump. expect($container.scrollTop()).toEqual(0) # See that the ghost for $new is aligned with the top edge # of the viewport. expect($newGhost.offset().top).toEqual(0) # The ghost for $old is shifted upwards to make it looks like it # was at the scroll position before we revealed $new. expect($oldGhost.offset().top).toEqual(-300) describe 'with animations disabled globally', -> beforeEach -> up.motion.config.enabled = false it "doesn't animate and hides the old element instead", -> $old = affix('.old').text('old content') $new = affix('.new').text('new content') up.morph($old, $new, 'cross-fade', duration: 1000) expect($old).toBeHidden() expect($new).toBeVisible() expect($new.css('opacity')).toEqual('1') describeFallback 'canCssTransition', -> it "doesn't animate and hides the old element instead", -> $old = affix('.old').text('old content') $new = affix('.new').text('new content') up.morph($old, $new, 'cross-fade', duration: 1000) expect($old).toBeHidden() expect($new).toBeVisible() expect($new.css('opacity')).toEqual('1') [false, null, undefined, 'none', up.motion.none()].forEach (noneTransition) -> describe "when called with a `#{noneTransition}` transition", -> it "doesn't animate and hides the old element instead", -> $old = affix('.old').text('old content') $new = affix('.new').text('new content') up.morph($old, $new, noneTransition, duration: 1000) expect($old).toBeHidden() expect($new).toBeVisible() expect($new.css('opacity')).toEqual('1') describe 'up.transition', -> it 'should have tests' describe 'up.animation', -> it 'should have tests' describe 'up.motion.none', -> it 'should have tests' describe 'up.motion.prependCopy', -> afterEach -> $('.up-bounds, .up-ghost, .fixture').remove() it 'clones the given element into a .up-ghost-bounds container and inserts it as a sibling before the element', -> $element = affix('.element').text('element text') up.motion.prependCopy($element) $bounds = $element.prev() expect($bounds).toExist() expect($bounds).toHaveClass('up-bounds') $ghost = $bounds.children(':first')# $ghost.find('.element') expect($ghost).toExist() expect($ghost).toHaveClass('element') expect($ghost).toHaveText('element text') it 'removes <script> tags from the cloned element', -> $element = affix('.element') $('<script></script>').appendTo($element) up.motion.prependCopy($element) $ghost = $('.up-ghost') expect($ghost.find('script')).not.toExist() it 'absolutely positions the ghost over the given element', -> $element = affix('.element') up.motion.prependCopy($element) $ghost = $('.up-ghost') expect($ghost.offset()).toEqual($element.offset()) expect($ghost.width()).toEqual($element.width()) expect($ghost.height()).toEqual($element.height()) it 'accurately positions the ghost over an element with margins', -> $element = affix('.element').css(margin: '40px') up.motion.prependCopy($element) $ghost = $('.up-ghost') expect($ghost.offset()).toEqual($element.offset()) it "doesn't change the position of a child whose margins no longer collapse", -> $element = affix('.element') $child = $('<div class="child"></div>').css(margin: '40px').appendTo($element) up.motion.prependCopy($element) $clonedChild = $('.up-ghost .child') expect($clonedChild.offset()).toEqual($child.offset()) it 'correctly positions the ghost over an element within a scrolled body', -> $body = $('body') $element1 = $('<div class="fixture"></div>').css(height: '75px').prependTo($body) $element2 = $('<div class="fixture"></div>').css(height: '100px').insertAfter($element1) $body.scrollTop(17) { $bounds, $ghost } = up.motion.prependCopy($element2) expect($bounds.css('position')).toBe('absolute') expect($bounds.css('top')).toEqual('75px') expect($ghost.css('position')).toBe('static') it 'correctly positions the ghost over an element within a viewport with overflow-y: scroll' it 'converts fixed elements within the copies to absolutely positioning', -> $element = affix('.element').css position: 'absolute' top: '50px' left: '50px' $fixedChild = $('<div class="fixed-child" up-fixed></div>').css position: 'fixed' left: '77px' top: '77px' $fixedChild.appendTo($element) up.motion.prependCopy($element, $('body')) $fixedChildGhost = $('.up-ghost .fixed-child') expect($fixedChildGhost.css(['position', 'left', 'top'])).toEqual position: 'absolute', left: '27px', top: '27px'