// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== // ======================================================================== // View Animation Unit Tests // ======================================================================== /*global module, test, ok, equals, stop, start, expect*/

/* These unit tests verify: animate(). */ var view, pane, originalSupportsTransitions = SC.platform.supportsCSSTransitions;

function styleFor(view) {

return view.get('layer').style;

}

function transitionFor(view) {

return styleFor(view)[SC.browser.experimentalStyleNameFor('transition')];

}

var commonSetup = {

setup: function (wantsAcceleratedLayer) {

  SC.run(function () {
    pane = SC.Pane.create({
      backgroundColor: '#ccc',
      layout: { top: 0, right: 0, width: 200, height: 200, zIndex: 100 }
    });
    pane.append();

    view = SC.View.create({
      backgroundColor: '#888',
      layout: { left: 0, top: 0, height: 100, width: 100 },
      wantsAcceleratedLayer: wantsAcceleratedLayer || NO
    });
    pane.appendChild(view);
  });
},

teardown: function () {
  SC.run(function () {
    pane.destroy();
  });
  pane = view = null;
}

};

if (SC.platform.supportsCSSTransitions) {

module("ANIMATION", commonSetup);

test("should work", function () {
  stop(4000);
  SC.RunLoop.begin();
  view.animate('left', 100, { duration: 1 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'left 1s ease 0s', 'add transition');
    equals(100, view.get('layout').left, 'left is 100');

    start();
  }, 5);
});

test("animate + adjust: no conflict", function () {
  stop(4000);

  SC.run(function () {
    view.animate('left', 100, { duration: 0.1 });
    view.adjust('top', 100);
    view.adjust({ 'width': null, 'right': 100 });
  });

  setTimeout(function () {
    equals(view.get('layout').left, 100, 'left is');
    equals(view.get('layout').top, 100, 'top is');
    equals(view.get('layout').right, 100, 'right is');
    equals(view.get('layout').width, undefined, 'width is');

    SC.run(function () {
      view.animate('top', 200, { duration: 0.1 });
      view.adjust('left', 0);
      view.adjust({ 'width': 100, 'right': null });
    });

    setTimeout(function () {
      equals(view.get('layout').left, 0, 'left is');
      equals(view.get('layout').top, 200, 'top is');
      equals(view.get('layout').right, undefined, 'right is');
      equals(view.get('layout').width, 100, 'width is');

      start();
    }, 200);
  }, 200);
});

test("animate + adjust: conflict", function () {
  stop(4000);

  SC.run(function () {
    view.animate('left', 100, { duration: 0.1 });
    view.adjust('left', 200);
  });

  setTimeout(function () {
    equals(view.get('layout').left, 200, 'left is');

    SC.run(function () {
      view.animate('top', 200, { duration: 0.1 });
      // Adjust back to current value should still cancel the animation.
      view.adjust('top', 0);
    });

    setTimeout(function () {
      equals(view.get('layout').top, 0, 'top is');

      start();
    }, 200);
  }, 200);
});

test("callbacks work in general", function () {
  stop(4000);

  SC.run(function () {
    view.animate('left', 100, { duration: 0.5 }, function () {
      ok(true, "Callback was called.");
      equals(view, this, "`this` should be the view");

      start();
    });
  });
});

test("callbacks work in general with target method", function () {
  stop(4000);

  var ob = SC.Object.create({
    callback: function () {
      ok(true, "Callback was called.");
      equals(ob, this, "`this` should be the target object");

      start();
    }
  });

  SC.run(function () {
    view.animate('left', 100, { duration: 0.5 }, ob, 'callback');
  });
});

test("callbacks should have appropriate data", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate('left', 100, { duration: 0.5 }, function (data) {
    // TODO: Test this better
    ok(data.event, "has event");
    equals(data.view, view, "view is correct");
    equals(data.isCancelled, false, "animation is not cancelled");

    start();
  });
  SC.RunLoop.end();
});

test("handles delay function string", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate('left', 100, { duration: 1, delay: 1 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'left 1s ease 1s', 'uses delay');

    start();
  }, 5);
});

test("handles timing function string", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate('left', 100, { duration: 1, timing: 'ease-in' });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'left 1s ease-in 0s', 'uses ease-in timing');

    start();
  }, 5);
});

test("handles timing function array", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate('left', 100, { duration: 1, timing: [0.1, 0.2, 0.3, 0.4] });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'left 1s cubic-bezier(0.1, 0.2, 0.3, 0.4) 0s', 'uses cubic-bezier timing');

    start();
  }, 5);
});

test("should allow multiple keys to be set at once", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate({ top: 100, left: 100 }, { duration: 1 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'top 1s ease 0s, left 1s ease 0s', 'should add transition');
    equals(100, view.get('layout').top, 'top is 100');
    equals(100, view.get('layout').left, 'left is 100');

    start();
  }, 5);
});

test("should not animate any keys that don't change", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate({ top: 0, left: 100 }, { duration: 1 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'left 1s ease 0s', 'should only add left transition');
    equals(0, view.get('layout').top, 'top is 0');
    equals(100, view.get('layout').left, 'left is 100');

    start();
  }, 5);
});

test("animating height with a centerY layout should also animate margin-top", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.adjust({ top: null, centerY: 0 });
  view.animate({ height: 10 }, { duration: 1 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'height 1s ease 0s, margin-top 1s ease 0s', 'should add height and margin-top transitions');
    equals(view.get('layout').height, 10, 'height');
    equals(view.get('layout').centerY, 0, 'centerY');

    start();
  }, 5);
});

test("animating width with a centerX layout should also animate margin-left", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.adjust({ left: null, centerX: 0 });
  view.animate({ width: 10 }, { duration: 1 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(transitionFor(view), 'width 1s ease 0s, margin-left 1s ease 0s', 'should add width and margin-left transitions');
    equals(view.get('layout').width, 10, 'width');
    equals(view.get('layout').centerX, 0, 'centerX');

    start();
  }, 5);
});

// Pretty sure this does the job
test("callbacks should be called only once for a grouped animation", function () {
  stop(4000);
  var stopped = true;

  expect(1);

  SC.run(function () {
    view.animate({ top: 100, left: 100, width: 400 }, { duration: 0.5 }, function () {
      ok(stopped, 'callback called back');
      if (stopped) {
        stopped = false;
        // Continue on in a short moment.  Before the test times out, but after
        // enough time for a second callback to possibly come in.
        setTimeout(function () {
          start();
        }, 200);
      }
    });
  });
});

// This behavior should be up for debate.  Does the callback call immediately, or does it wait until the end of
// the specified animation period?  Currently we're calling it immediately.
test("callback should be called immediately when a property is animated to its current value.", function () {
  stop(4000);

  expect(1);

  SC.run(function () {
    view.animate('top', view.getPath('layout.top'), { duration: 0.5 }, function () {
      ok(true, 'callback called back');

      start();
    });
  });
});

// This behavior should be up for debate.  Does the callback call immediately, or does it wait until the end of
// the specified animation period?  Currently we're calling it immediately.
test("callback should be called immediately when a property is animated to its current value (even if the value is implied).", function () {
  stop(4000);

  expect(1);

  var implicitView = SC.View.create({
      backgroundColor: '#ABC',
      layout: { left: 0 } // implicit layout: { top: 0, right: 0, bottom: 0 }
    });
  pane.appendChild(implicitView);

  SC.run(function () {
    implicitView.animate('top', 0, { duration: 0.5 }, function () {
      ok(true, 'callback called back');

      start();
    });
  });
});

test("callback should be called when a property is animated with a duration of zero.", function () {
  stop(4000);

  expect(1);

  SC.RunLoop.begin();
  view.animate('top', 20, { duration: 0 }, function () {
    ok(true, 'callback called back');
    start();
  });
  SC.RunLoop.end();
});

test("multiple animations should be able to run simultaneously", function () {
  stop(4000);

  expect(2);

  SC.run(function () {
    view.animate('top', 100, { duration: 0.25 }, function () {
      ok(true, 'top finished');
    });

    view.animate('left', 100, { duration: 0.5 }, function () {
      ok(true, 'left finished');
      start();
    });
  });
});

test("altering existing animation should call callback as cancelled", function () {
  stop(4000);

  var order = 0;
  expect(6);

  SC.run(function () {
    view.animate('top', 100, { duration: 0.5 }, function (data) {
      // Test the order to ensure that this is the proper callback that is used.
      equals(order, 0, 'should be called first');
      order = 1;
      equals(data.isCancelled, true, 'first cancelled');
    });

    // Test calling animate twice in the same run loop.
    view.animate('top', 100, { duration: 0.75 }, function (data) {
      // Test the order to ensure that this is the proper callback that is used.
      equals(order, 1, 'should be called second');
      order = 2;
      equals(data.isCancelled, true, 'second cancelled');
    });
  });

  setTimeout(function () {
    SC.run(function () {
      view.animate('top', 0, { duration: 0.75 }, function (data) {
        // Test the order to ensure that this is the proper callback that is used.
        equals(order, 2, 'should be called third');
        equals(data.isCancelled, false, 'third not cancelled');
        start();
      });
    });
  }, 100);
});

test("should not cancel callback when value hasn't changed", function () {
  var callbacks = 0, wasCancelled = NO, check = 0;
  stop(4000);

  SC.run(function () {
    // this triggers the initial layoutStyle code
    view.animate('left', 79, { duration: 0.5 }, function (data) {
      callbacks++;
      wasCancelled = data.isCancelled;
    });

    // this triggers a re-render, re-running the layoutStyle code
    view.displayDidChange();
  });

  setTimeout(function () {
    // capture the callbacks value
    check = callbacks;
  }, 250);

  setTimeout(function () {
    equals(check, 0, "the callback should not have been cancelled initially");
    equals(callbacks, 1, "the callback should have been fired");
    equals(wasCancelled, NO, "the callback should not have been cancelled");

    start();
  }, 1000);
});

// There was a bug in animation that once one property was animated, a null
// version of it existed in _activeAnimations, such that when another property
// was animated it would throw an exception iterating through _activeAnimations
// and not expecting a null value.
test("animating different attributes at different times should not throw an error", function () {
  // Run test.
  stop(4000);

  expect(0);

  // Override and wrap the problematic method to capture the error.
  view.transitionDidEnd = function () {
    try {
      SC.View.prototype.transitionDidEnd.apply(this, arguments);
      ok(true);
    } catch (ex) {
      ok(false);
    }
  };

  SC.RunLoop.begin();
  view.animate('left', 75, { duration: 0.2 });
  SC.RunLoop.end();

  setTimeout(function () {
    SC.RunLoop.begin();
    view.animate('top', 50, { duration: 0.2 });
    SC.RunLoop.end();
  }, 400);

  setTimeout(function () {
    start();
  }, 1000);
});

test("should handle transform attributes", function () {
  stop(4000);

  SC.run(function () {
    view.animate('rotateX', 45, { duration: 1 });
  });

  setTimeout(function () {
    equals(transitionFor(view), SC.browser.experimentalCSSNameFor('transform') + ' 1s ease 0s', 'add transition');
    equals(styleFor(view)[SC.browser.experimentalStyleNameFor('transform')], 'rotateX(45deg)', 'has both transforms');
    equals(45, view.get('layout').rotateX, 'rotateX is 45deg');

    start();
  }, 50);
});

test("should handle conflicting transform animations", function () {
  /*global console*/
  stop(4000);

  var originalConsoleWarn = console.warn;
  console.warn = function (warning) {
    equals(warning, "Developer Warning: Can't animate transforms with different durations, timings or delays! Using the first options specified.", "proper warning");
  };

  SC.run(function () {
    view.animate('rotateX', 45, { duration: 1 }).animate('scale', 2, { duration: 2 });
  });

  setTimeout(function () {
    expect(5);

    equals(transitionFor(view), SC.browser.experimentalCSSNameFor('transform') + ' 1s ease 0s', 'use duration of first');
    equals(styleFor(view)[SC.browser.experimentalStyleNameFor('transform')], 'rotateX(45deg) scale(2)');
    equals(45, view.get('layout').rotateX, 'rotateX is 45deg');
    equals(2, view.get('layout').scale, 'scale is 2');

    console.warn = originalConsoleWarn;

    start();
  }, 25);
});

test("removes animation property when done", function () {
  stop(4000);

  SC.RunLoop.begin();
  view.animate({ top: 100, scale: 2 }, { duration: 0.5 });
  SC.RunLoop.end();

  setTimeout(function () {
    equals(view.get('layout').animateTop, undefined, "animateTop is undefined");
    equals(view.get('layout').animateScale, undefined, "animateScale is undefined");

    start();
  }, 1000);
});

test("Test that cancelAnimation() removes the animation style and fires the callback with isCancelled set.", function () {
  stop(4000);

  expect(7);

  SC.run(function () {
    view.animate({ left: 100 }, { duration: 0.5 }, function (data) {
      ok(data.isCancelled, "The isCancelled property of the data should be true.");
    });
  });

  setTimeout(function () {
    SC.run(function () {
      var style = styleFor(view);

      equals(style.left, '100px', 'Tests the left style after animate');
      equals(style.top, '0px', 'Tests the top style after animate');
      equals(transitionFor(view), 'left 0.5s ease 0s', 'Tests the CSS transition property');
      view.cancelAnimation();
    });
  }, 5);

  setTimeout(function () {
    var style = styleFor(view);

    equals(style.left, '100px', 'Tests the left style after cancel');
    equals(style.top, '0px', 'Tests the top style after cancel');
    equals(transitionFor(view), '', 'Tests the CSS transition property');
    start();
  }, 50);
});

test("Test that cancelAnimation(SC.LayoutState.CURRENT) removes the animation style, stops at the current position and fires the callback with isCancelled set.", function () {
  stop(4000);

  expect(9);

  SC.run(function () {
    view.animate({ left: 100, top: 100, width: 400 }, { duration: 0.5 }, function (data) {
      ok(data.isCancelled, "The isCancelled property of the data should be true.");
    });
  });

  setTimeout(function () {
    SC.run(function () {
      var style = styleFor(view);

      equals(style.left, '100px', 'Tests the left style after animate');
      equals(style.top, '100px', 'Tests the top style after animate');
      equals(style.width, '400px', 'Tests the width style after animate');
      equals(transitionFor(view), 'left 0.5s ease 0s, top 0.5s ease 0s, width 0.5s ease 0s', 'Tests the CSS transition property');
      view.cancelAnimation(SC.LayoutState.CURRENT);
    });
  }, 100);

  setTimeout(function () {
    var style = styleFor(view);

    ok((parseInt(style.left, 10) > 0) && (parseInt(style.left, 10) < 100), 'Tests the left style after cancel');
    ok((parseInt(style.top, 10) > 0) && (parseInt(style.top, 10) < 100), 'Tests the top style after cancel');
    ok((parseInt(style.width, 10) > 100) && (parseInt(style.width, 10) < 400), 'Tests the width style after cancel');
    equals(transitionFor(view), '', 'Tests the CSS transition property');
    start();
  }, 200);
});

test("Test that cancelAnimation(SC.LayoutState.START) removes the animation style, returns to the start position and fires the callback with isCancelled set.", function () {
  stop(4000);

  expect(9);

  SC.run(function () {
    view.animate({ left: 100, top: 100, width: 400 }, { duration: 0.5 }, function (data) {
      ok(data.isCancelled, "The isCancelled property of the data should be true.");
    });
  });

  setTimeout(function () {
    SC.run(function () {
      var style = styleFor(view);

      equals(style.left, '100px', 'Tests the left style after animate');
      equals(style.top, '100px', 'Tests the top style after animate');
      equals(style.width, '400px', 'Tests the width style after animate');
      equals(transitionFor(view), 'left 0.5s ease 0s, top 0.5s ease 0s, width 0.5s ease 0s', 'Tests the CSS transition property');
      view.cancelAnimation(SC.LayoutState.START);
    });
  }, 100);

  setTimeout(function () {
    var style = styleFor(view);

    equals(style.left, '0px', 'Tests the left style after cancel');
    equals(style.top, '0px', 'Tests the top style after cancel');
    equals(style.width, '100px', 'Tests the width style after animate');
    equals(transitionFor(view), '', 'Tests the CSS transition property');
    start();
  }, 200);
});

if (SC.platform.supportsCSS3DTransforms) {
  module("ANIMATION WITH ACCELERATED LAYER", {
    setup: function () {
      commonSetup.setup(YES);
    },

    teardown: commonSetup.teardown
  });

  test("handles acceleration when appropriate", function () {
    stop(4000);

    SC.RunLoop.begin();
    view.animate('top', 100, { duration: 1 });
    SC.RunLoop.end();

    setTimeout(function () {
      equals(transitionFor(view), SC.browser.experimentalCSSNameFor('transform') + ' 1s ease 0s', 'transition is on transform');

      start();
    }, 5);
  });

  test("doesn't use acceleration when not appropriate", function () {
    stop(4000);

    SC.RunLoop.begin();
    view.adjust({ height: null, bottom: 0 });
    view.animate('top', 100, { duration: 1 });
    SC.RunLoop.end();

    setTimeout(function () {
      equals(transitionFor(view), 'top 1s ease 0s', 'transition is not on transform');

      start();
    }, 5);
  });

  test("combines accelerated layer animation with compatible transform animations", function () {
    stop(4000);

    SC.RunLoop.begin();
    view.animate('top', 100, { duration: 1 }).animate('rotateX', 45, { duration: 1 });
    SC.RunLoop.end();

    setTimeout(function () {
      var transform = styleFor(view)[SC.browser.experimentalStyleNameFor('transform')];

      // We need to check these separately because in some cases we'll also have translateZ, this way we don't have to worry about it
      ok(transform.match(/translateX\(0px\) translateY\(100px\)/), 'has translate');
      ok(transform.match(/rotateX\(45deg\)/), 'has rotateX');

      start();
    }, 5);
  });

  test("should not use accelerated layer if other transforms are being animated at different speeds", function () {
    stop(4000);

    SC.RunLoop.begin();
    view.animate('rotateX', 45, { duration: 2 }).animate('top', 100, { duration: 1 });
    SC.RunLoop.end();

    setTimeout(function () {
      var style = styleFor(view);

      equals(style[SC.browser.experimentalStyleNameFor('transform')], 'rotateX(45deg)', 'transform should only have rotateX');
      equals(style.top, '100px', 'should not accelerate top');

      start();
    }, 5);
  });

  test("callbacks should work properly with acceleration", function () {
    stop(4000);

    SC.run(function () {
      view.animate({ top: 100, left: 100, scale: 2 }, { duration: 0.25 }, function () {
        ok(true);

        start();
      });
    });
  });

  test("should not add animation for properties that have the same value as existing layout", function () {
    var callbacks = 0;

    SC.RunLoop.begin();
    // we set width to the same value, but we change height
    view.animate({width: 100, height: 50}, { duration: 0.5 }, function () { callbacks++; });
    SC.RunLoop.end();

    ok(callbacks === 0, "precond - callback should not have been run yet");

    stop(4000);

    // we need to test changing the width at a later time
    setTimeout(function () {
      start();

      equals(callbacks, 1, "callback should have been run once, for height change");

      SC.RunLoop.begin();
      view.animate('width', 50, { duration: 0.5 });
      SC.RunLoop.end();

      equals(callbacks, 1, "callback should still have only been called once, even though width has now been animated");
    }, 1000);
  });

  test("Test that cancelAnimation() removes the animation style and fires the callback with isCancelled set.", function () {
    stop(4000);

    SC.run(function () {
      view.animate({ left: 100, top: 100, width: 400 }, { duration: 0.5 }, function (data) {
        ok(data.isCancelled, "The isCancelled property of the data should be true.");
      });
    });

    setTimeout(function () {
      SC.run(function () {
        var style = styleFor(view),
        transform = style[SC.browser.experimentalStyleNameFor('transform')];
        transform = transform.match(/\d+/g);

        // We need to check these separately because in some cases we'll also have translateZ, this way we don't have to worry about it
        equals(transform[0], '100',  "Test translateX after animate.");
        equals(transform[1], '100',  "Test translateY after animate.");

        equals(transitionFor(view), SC.browser.experimentalCSSNameFor('transform') + ' 0.5s ease 0s, width 0.5s ease 0s', 'Tests the CSS transition property');

        equals(style.left, '0px', 'Tests the left style after animate');
        equals(style.top, '0px', 'Tests the top style after animate');
        equals(style.width, '400px', 'Tests the width style after animate');

        view.cancelAnimation();
      });
    }, 250);

    setTimeout(function () {
      var style = styleFor(view);
      equals(style.width, '400px', 'Tests the width style after cancel');

      var transform = style[SC.browser.experimentalStyleNameFor('transform')];
      transform = transform.match(/\d+/g);

      equals(transform[0], '100',  "Test translateX after cancel.");
      equals(transform[1], '100',  "Test translateY after cancel.");

      equals(transitionFor(view), '', 'Tests that there is no CSS transition property after cancel');

      start();
    }, 350);
  });

  test("Test that cancelAnimation(SC.LayoutState.CURRENT) removes the animation style, stops at the current position and fires the callback with isCancelled set.", function () {
    stop(4000);

    SC.run(function () {
      view.animate({ left: 200, top: 200, width: 400 }, { duration: 1 }, function (data) {
        ok(data.isCancelled, "The isCancelled property of the data should be true.");
      });
    });

    setTimeout(function () {
      SC.run(function () {
        var style = styleFor(view),
        transform = style[SC.browser.experimentalStyleNameFor('transform')];
        transform = transform.match(/\d+/g);

        // We need to check these separately because in some cases we'll also have translateZ, this way we don't have to worry about it
        equals(transform[0], '200',  "Test translateX after animate.");
        equals(transform[1], '200',  "Test translateY after animate.");
        equals(transitionFor(view), SC.browser.experimentalCSSNameFor('transform') + ' 1s ease 0s, width 1s ease 0s', 'Tests the CSS transition property');

        equals(style.left, '0px', 'Tests the left style after animate');
        equals(style.top, '0px', 'Tests the top style after animate');
        equals(style.width, '400px', 'Tests the width style after animate');

        view.cancelAnimation(SC.LayoutState.CURRENT);
      });
    }, 250);

    setTimeout(function () {
      var style = styleFor(view),
        layout = view.get('layout');

      equals(transitionFor(view), '', 'Tests that there is no CSS transition property after cancel');

      // We need to check these separately because in some cases we'll also have translateZ, this way we don't have to worry about it
      ok((layout.left > 0) && (layout.left < 200), 'Tests the left style, %@, after cancel is greater than 0 and less than 200'.fmt(style.left));
      ok((layout.top > 0) && (layout.top < 200), 'Tests the top style, %@, after cancel is greater than 0 and less than 200'.fmt(style.top));
      ok((parseInt(style.width, 10) > 100) && (parseInt(style.width, 10) < 400), 'Tests the width style, %@, after cancel is greater than 100 and less than 400'.fmt(style.width));
      start();
    }, 750);
  });

  test("Test that cancelAnimation(SC.LayoutState.START) removes the animation style, goes back to the start position and fires the callback with isCancelled set.", function () {
    stop(4000);

    // expect(12);

    SC.run(function () {
      view.animate({ left: 100, top: 100, width: 400 }, { duration: 0.5 }, function (data) {
        ok(data.isCancelled, "The isCancelled property of the data should be true.");
      });
    });

    setTimeout(function () {
      SC.run(function () {
        var style = styleFor(view),
        transform = style[SC.browser.experimentalStyleNameFor('transform')];
        equals(style.left, '0px', 'Tests the left style after animate');
        equals(style.top, '0px', 'Tests the top style after animate');
        equals(style.width, '400px', 'Tests the width style after animate');

        transform = transform.match(/\d+/g);

        // We need to check these separately because in some cases we'll also have translateZ, this way we don't have to worry about it
        equals(transform[0], '100',  "Test translateX after animate.");
        equals(transform[1], '100',  "Test translateY after animate.");

        equals(transitionFor(view), SC.browser.experimentalCSSNameFor('transform') + ' 0.5s ease 0s, width 0.5s ease 0s', 'Tests the CSS transition property');
        view.cancelAnimation(SC.LayoutState.START);
      });
    }, 250);

    setTimeout(function () {
      var style = styleFor(view);

      var transform = style[SC.browser.experimentalStyleNameFor('transform')];
      transform = transform.match(/\d+/g);

      equals(transitionFor(view), '', 'Tests that there is no CSS transition property after cancel');

      // We need to check these separately because in some cases we'll also have translateZ, this way we don't have to worry about it
      equals(transform[0], '0',  "Test translateX after cancel.");
      equals(transform[1], '0',  "Test translateY after cancel.");
      equals(style.width, '100px', 'Tests the width style after cancel');
      start();
    }, 350);
  });
} else {
  test("This platform appears to not support CSS 3D transforms.");
}

} else {

test("This platform appears to not support CSS transitions.");

}

module(“ANIMATION WITHOUT TRANSITIONS”, {

setup: function () {
  commonSetup.setup();
  SC.platform.supportsCSSTransitions = NO;
},

teardown: function () {
  commonSetup.teardown();
  SC.platform.supportsCSSTransitions = originalSupportsTransitions;
}

});

test(“should update layout”, function () {

stop(4000);
SC.RunLoop.begin();
view.animate('left', 100, { duration: 1 });
SC.RunLoop.end();

setTimeout(function () {
  equals(view.get('layout').left, 100, 'left is 100');
  start();
}, 5);

});

test(“should still run callback”, function () {

stop(4000);

expect(1);

SC.RunLoop.begin();
view.animate({ top: 200, left: 100 }, { duration: 1 }, function () {
  ok(true, "callback called");
  start();
});
SC.RunLoop.end();

});

module(“Animating in the next run loop”, commonSetup);

test(“Calling animate while flusing the invokeNext queue should not throw an exception”, function () {

try {
  SC.run(function () {
    view.invokeNext(function () {
      this.animate({ top: 250 }, { duration: 1 });
    });

    view.animate({ top: 200 }, { duration: 1 });
  });

  SC.run(function () {
    // The first call to _animate and the function with animate in it run.
  });

  SC.run(function () {
    // The second call to _animate from the function with animate in it.
  });
} catch (ex) {
  ok(false, "failure");
}

ok(true, "success");

});