// ========================================================================== // Project: SproutCore
- JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // portions copyright @2011 Apple Inc. // License: Licensed under MIT license (see license.js) // ==========================================================================
/*global module, test, ok, equals */
var pane, scrollView, inner, targetLayer, evt, evt2, evt3,
scrollStart, innerStart, scrollDragged, innerDragged, scrollEnd, innerEnd, scrollCancel, innerCancel;
// Sets up and tears down our pane and event. module(“SC.ScrollView touch”, {
setup: function() { // Initialize our counters. scrollStart = scrollDragged = scrollEnd = scrollCancel = 0; innerStart = innerDragged = innerEnd = innerCancel = 0; // Create our pane. pane = SC.MainPane.extend({ layout: { height: 80 }, childViews: ['contentView'], contentView: SC.ScrollView.extend({ touchStart: function() { scrollStart++; return sc_super(); }, touchesDragged: function() { scrollDragged++; return sc_super(); }, touchEnd: function() { scrollEnd++; return sc_super(); }, touchCancelled: function() { scrollCancel++; return sc_super(); }, contentView: SC.View.extend({ layout: { height: 100 }, touchStart: function() { innerStart++; }, touchesDragged: function(evt, touchesForView) { innerDragged++; // If we've scrolled more than 15 pixels, pass back to the scroller. if (Math.abs(evt.startY - evt.pageY) > 15) { touchesForView.invoke('restoreLastTouchResponder'); } }, touchEnd: function() { innerEnd++; }, touchCancelled: function() { innerCancel++; } }) }) }); // (Actually create it, in a run loop.) SC.run(function() { pane = pane.create().append(); }); // Set up our pertinent reused variables. scrollView = pane.contentView; inner = scrollView.contentView; targetLayer = inner.get('layer'); evt = SC.Event.simulateEvent(targetLayer, 'touchstart'); evt.touches = [evt]; evt.identifier = 4; evt.pageX = 50; evt.pageY = 50; evt.changedTouches = [evt]; }, teardown: function() { SC.run(pane.destroy, pane); pane = scrollView = inner = targetLayer = evt = null; }
});
// Test a touch lifecycle with no vertical movement and delaysContentTouches: YES - the scroll view will capture the touch, // but since there was no scroll, it will give inner views a chance to respond to it on touchEnd. test(“Tapping with delaysContentTouches: YES”, function() {
// Trigger touchstart SC.Event.trigger(targetLayer, 'touchstart', evt); equals(scrollStart, 1, "After touchstart, the scroll view's touchStart should have been called once"); equals(innerStart, 0, "After touchstart, the inner view's touchStart should not have been called, as the touch was captured by the scroll view"); // Trigger touchmove: SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollDragged, 1, "After touchmove, the scroll view's touchesDragged should have been called once"); equals(innerDragged, 0, "After touchmove, the inner view's touchesDragged should not have been called, as the touch is still owned by the scroll view"); // Trigger touchend: SC.Event.trigger(targetLayer, 'touchend', evt); equals(scrollEnd, 1, "After touchend, the scroll view's touchEnd should have been called once"); equals(innerStart, 1, "Once the scroll view has handled touchEnd, it passes the touch to the inner view, so innerStart should have run"); equals(innerDragged, 0, "The inner view's touchesDragged method should still not have been called"); equals(innerEnd, 1, "The scroll view ends the touch as soon as the inner view has had a chance to start it, thus tap; this triggers the inner view's touchEnd immediately");
});
// Test a touch lifecycle with some vertical movement and delaysContentTouches: YES - The scroll view will capture the // touch, and since there was a scroll, the inner view will not receive any notifications whatsoever. test(“Dragging with delaysContentTouches: YES”, function() {
// Trigger touchstart SC.Event.trigger(targetLayer, 'touchstart', evt); equals(scrollStart, 1, "After touchstart, the scroll view's touchStart should have been called once"); equals(innerStart, 0, "After touchstart, the inner view's touchStart should not have been called, as the touch was captured by the scroll view"); // Give the event some vertical delta: evt.pageY += 16; // Trigger touchmove: SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollDragged, 1, "After touchmove, the scroll view's touchesDragged should have been called once"); equals(innerDragged, 0, "After touchmove, the inner view's touchesDragged should not have been called, as the touch is still owned by the scroll view"); // Trigger touchend: SC.Event.trigger(targetLayer, 'touchend', evt); equals(scrollEnd, 1, "After touchend, the scroll view's touchEnd should have been called once"); equals(innerStart, 0, "inner view's touchStart will not have run, as the touch has moved enough to begin scrolling and will bypass the inner view entirely"); equals(innerDragged, 0, "The inner view's touchesDragged method should still not have been called"); equals(innerEnd, 0, "Having never started, the inner view will not receive touchEnd either");
});
// Test a touch lifecycle with no vertical movement and delaysContentTouches: NO - the scroll view should not partake in this touch at all. test(“Tapping with delaysContentTouches: NO”, function() {
scrollView.set('delaysContentTouches', NO); // Trigger touchstart SC.Event.trigger(targetLayer, 'touchstart', evt); equals(scrollStart, 0, "We're not capturing touches, so scroll view's touchStart will not have fired after touchstart"); equals(innerStart, 1, "After touchstart, inner view's touchStart will have been called once"); // Trigger touchmove: SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollDragged, 0, "Scroll view's touchesDragged will not have fired, as it is not the touch responder"); equals(innerDragged, 1, "After touchmove, inner view's touchDragged gets straightforwardly called because it is the touch responder"); // Trigger touchend: SC.Event.trigger(targetLayer, 'touchend', evt); equals(scrollEnd, 0, "Again, the scroll view is completely uninvolved in this touch, so its touchEnd doesn't get called"); equals(innerEnd, 1, "The inner view's touchEnd gets called because of how it's responding to the touch");
});
// Tests a touch lifecycle with some vertical movement and delaysContentTouches: NO - the inner view will receive the touch, // but upon it becoming a drag will voluntarily relinquish it back to the scroll view. (See innerView.touchesDragged in the // current module.setup method.) test(“Dragging with delaysContentTouches: NO”, function() {
scrollView.set('delaysContentTouches', NO); // Trigger touchstart SC.Event.trigger(targetLayer, 'touchstart', evt); equals(scrollStart, 0, "Since the scroll view isn't capturing touches, it gets no touchStart love"); equals(innerStart, 1, "After touchstart, the inner view's touchStart should have been straightforwardly called"); // Give the event some vertical delta: evt.pageY += 16; // Trigger touchmove: SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollDragged, 0, "The scroll view's touchDragged should not have been called, since at the time of the event it was not the touch's responder"); equals(innerDragged, 1, "The inner view's touchesDragged should have straightforwardly handled the event"); equals(scrollStart, 1, "Having been passed the touch by inner view's touchesDragged, the scroll view's touchStart will now have fired"); equals(innerCancel, 1, "Having passed the touch back to the scroll view, the inner view's touchCancelled should have run"); // Trigger touchend: SC.Event.trigger(targetLayer, 'touchend', evt); equals(scrollEnd, 1, "After touchend, the scroll view's touchEnd should have been called once");
});
var initialPageX = 100,
initialPageY = 100;
module(“SC.ScrollView touch thresholds and locks”, {
setup: function() { // Set up our pane (then create it in a run loop). pane = SC.MainPane.extend({ layout: { height: 100, width: 100 }, childViews: ['contentView'], contentView: SC.ScrollView.extend({ touchScrollThreshold: 10, touchSecondaryScrollThreshold: 20, touchSecondaryScrollLock: 30, horizontalAlign: SC.ALIGN_LEFT, contentView: SC.View.extend({ layout: { height: 200, width: 200 } }) }) }); SC.run(function() { pane = pane.create().append(); }); // Set up our pertinent reused variables. scrollView = pane.contentView; inner = scrollView.contentView; targetLayer = inner.get('layer'); evt = SC.Event.simulateEvent(targetLayer, 'touchstart'); evt.touches = [evt]; evt.identifier = 4; evt.pageX = initialPageX; evt.pageY = initialPageY; evt.changedTouches = [evt]; }, teardown: function() { SC.run(pane.destroy, pane); }
});
// Disabled. The scroll thresholds don't stop the content from moving, they only allow the touch event to be // sent to the content if not met. This allows scrolling to begin immediately, not after a small pause. // test(“Touch scroll thresholds”, function() { // equals(scrollView.get('verticalScrollOffset'), 0, “PRELIM: Vertical offset starts at”); // equals(scrollView.get('horizontalScrollOffset'), 0, “PRELIM: Horizontal offset starts at”);
// // Start touch // SC.run(function() { // SC
.Event.trigger(targetLayer, 'touchstart', evt); // });
// // Move touch up less than touchScrollThreshold. // evt.pageY = initialPageY - 9; // SC.run(function() { // SC
.Event.trigger(targetLayer, 'touchmove', evt); // }); // equals(scrollView.get('verticalScrollOffset'), 0, “Scrolling less than touchScrollThreshold results in no scrolling”); // if (scrollView.get('horizontalScrollOffset') !== 0) ok(false, “A touch with no horizontal change shouldn't trigger a horizontal scroll!”);
// // Move touch up more than touchScrollThreshold. // evt.pageY = initialPageY - 11; // SC.run(function() { // SC
.Event.trigger(targetLayer, 'touchmove', evt); // }); // equals(scrollView.get('verticalScrollOffset'), 11, “Scrolling more than touchScrollThreshold results in scrolling”); // if (scrollView.get('horizontalScrollOffset') !== 0) ok(false, “A touch with no horizontal change shouldn't trigger a horizontal scroll!”);
// // Move touch sideways less than touchSecondaryScrollThreshold. // evt.pageX = initialPageX - 19; // SC.run(function() { // SC
.Event.trigger(targetLayer, 'touchmove', evt); // }); // if (scrollView.get('verticalScrollOffset') !== 11) ok(false, “A touch with no vertical change shouldn't trigger a vertical scroll!”); // equals(scrollView.get('horizontalScrollOffset'), 0, “With a vertical scroll in motion, scrolling horizontally less than touchSecondaryScrollThreshold results in no scrolling”);
// // Move touch sideways more than touchSecondaryScrollThreshold. // evt.pageX = initialPageX - 21; // SC.run(function() { // SC
.Event.trigger(targetLayer, 'touchmove', evt); // }); // if (scrollView.get('verticalScrollOffset') !== 11) ok(false, “A touch with no vertical change shouldn't trigger a vertical scroll!”); // equals(scrollView.get('horizontalScrollOffset'), 21, “With a vertical scroll in motion, scrolling horizontally by more than touchSecondaryScrollThreshold results in scrolling”);
// });
test(“Touch scroll lock”, function() {
equals(scrollView.get('verticalScrollOffset'), 0, "PRELIM: Vertical offset starts at"); equals(scrollView.get('horizontalScrollOffset'), 0, "PRELIM: Horizontal offset starts at"); // Start touch SC.Event.trigger(targetLayer, 'touchstart', evt); // Move touch up more than touchSecondaryScrollLock. evt.pageY = initialPageY - SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('verticalScrollOffset'), SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD, "PRELIM: Scrolling more than touchScrollThreshold results in scrolling"); equals(scrollView.get('horizontalScrollOffset'), 0, "A touch with no horizontal change shouldn't trigger a horizontal scroll!"); // Move touch sideways. evt.pageX = initialPageX - 50; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('verticalScrollOffset'), SC.SCROLL.SCROLL_LOCK_GESTURE_THRESHOLD, "A touch with no vertical change shouldn't trigger a vertical scroll!"); equals(scrollView.get('horizontalScrollOffset'), 0, "Having scrolled vertically past the scrollGestureSecondaryThreshold, horizontal touch movements are ignored");
});
module(“SC.ScrollView touch scale”, {
setup: function() { // Create our pane. pane = SC.MainPane.extend({ childViews: ['contentView'], contentView: SC.ScrollView.extend({ layout: { height: 1000, width: 1000 }, canScale: YES, horizontalOverlay: YES, verticalOverlay: YES, contentView: SC.View.extend({ layout: { height: 1000, width: 1000 } }) }) }); // (Actually create it, in a run loop.) SC.run(function() { pane = pane.create().append(); }); // Set up our pertinent reused variables. scrollView = pane.contentView; inner = scrollView.contentView; targetLayer = inner.get('layer'); evt = SC.Event.simulateEvent(targetLayer, 'touchstart'); evt.identifier = 4; evt.pageX = 400; evt.pageY = 400; evt2 = SC.Event.simulateEvent(targetLayer, 'touchstart'); evt2.identifier = 5; evt2.pageX = 600; evt2.pageY = 600; evt3 = SC.Event.simulateEvent(targetLayer, 'touchstart'); evt3.identifier = 6; evt3.pageX = 600; evt3.pageY = 400; }, teardown: function() { SC.run(pane.destroy, pane); }
});
/**
Some quick earned wisdom on testing touches. When you use SC.Event.trigger to trigger a touch event, the touch event's `touches` property must be an array with all of the currently operational touches (i.e. those previously started and not yet ended); its `changedTouches` property must be all the touches whose changes you want the root responder to acknowledge. `touchs` and `changedTouches` only need to be present on the event that you're passing to SC.Event.trigger; you trigger changes to multiple touches in one event by including them in that touch's `changedTouches` property. All `changedTouches` will be used to trigger the same event (for example, 'touchStart'), which can lead to testing issues if you pass the incorrect touches.
*/
test(“Basic touch scale”, function() {
equals(scrollView.get('verticalScrollOffset'), 0, "PRELIM: Vertical offset starts at"); equals(scrollView.get('horizontalScrollOffset'), 0, "PRELIM: Horizontal offset starts at"); equals(scrollView.get('scale'), 1, "PRELIM: Horizontal offset starts at"); // Start touches. evt.touches = []; evt.changedTouches = [evt, evt2]; SC.Event.trigger(targetLayer, 'touchstart', evt); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 2, "Two touches should result in two touches"); // Pinch out to 2x. evt.pageX = evt.pageY -= 100; evt2.pageX = evt2.pageY += 100; evt.touches = [evt, evt2]; // evt.changedTouches = [touch1, touch2]; SC.Event.trigger(targetLayer, 'touchmove', evt); // SC.ScrollView's touch-pinching depends heavily on SC.RootResponder#averagedTouchesForView. If these tests // are misbehaving, first verify that SC.RootResponder's touch tests are passing. equals(scrollView.get('scale'), 2, "A 2x pinch gesture should double the scroll's scale"); equals(scrollView.get('horizontalScrollOffset'), 500, "A centered pinch gesture should move the horizontal offset by half the content view's change in width"); equals(scrollView.get('verticalScrollOffset'), 500, "A centered pinch gesture should move the vertical offset by half the content view's change in height"); // Move the gesture. evt.pageX = evt.pageY += 100; evt2.pageX = evt2.pageY += 100; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('scale'), 2, "A gesture change with no change in distance should not change scale"); equals(scrollView.get('horizontalScrollOffset'), 400, "Gesture change in position by 100 should move offsets by 100"); equals(scrollView.get('verticalScrollOffset'), 400, "Gesture change in position by 100 should move offsets by 100"); // Move and pinch (back to 1x) in the same gesture. evt.pageX = evt.pageY = 400; evt2.pageX = evt2.pageY = 600; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('scale'), 1, "A pinch + move gesture should change the scale"); equals(scrollView.get('horizontalScrollOffset'), 0, "A pinch + move gesture should update the horizontal offset correctly"); equals(scrollView.get('verticalScrollOffset'), 0, "A pinch + move gesture should update the vertical offset correctly");
});
test(“Adding and removing touches (no scaling).”, function() {
// For this test we need room to scroll. SC.run(function() { inner.adjust('height', 2000); }); equals(scrollView.get('verticalScrollOffset'), 0, "PRELIM: Vertical offset starts at"); equals(scrollView.get('maximumVerticalScrollOffset'), 1000, "PRELIM: Vertical offset has room to grow"); equals(scrollView.get('horizontalScrollOffset'), 0, "PRELIM: Horizontal offset starts at"); equals(scrollView.get('scale'), 1, "PRELIM: Horizontal offset starts at"); // Start touches. evt.touches = []; evt.changedTouches = [evt, evt2]; SC.Event.trigger(targetLayer, 'touchstart', evt); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 2, "Two touches should result in two touches"); evt.pageY -= 100; evt2.pageY -= 100; evt.touches = [evt, evt2]; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 2, "There should still be two touches"); equals(scrollView.get('scale'), 1, "A two-touch gesture with no pinching should result in no scaling"); equals(scrollView.get('horizontalScrollOffset'), 0, "A two-touch vertical scroll gesture should not scroll horizontally"); equals(scrollView.get('verticalScrollOffset'), 100, "A two-touch vertical scroll gesture should successfully scroll vertically"); // Add a third touch. evt.touches = [evt, evt2]; evt.changedTouches = [evt3]; SC.Event.trigger(targetLayer, 'touchstart', evt); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 3, "Adding a third touch should result in three touches"); equals(scrollView.get('scale'), 1, "Adding a third touch should not impact scaling"); equals(scrollView.get('horizontalScrollOffset'), 0, "Adding a third touch should not impact horizontal offset"); equals(scrollView.get('verticalScrollOffset'), 100, "Adding a third touch should not impact vertical offset"); // Move all three touches up in tandem. evt.pageY -= 100; evt2.pageY -= 100; evt3.pageY -= 100; evt.touches = [evt, evt2, evt3]; evt.changedTouches = [evt, evt2, evt3]; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('scale'), 1, "A three-touch gesture with no pinching should result in no scaling"); equals(scrollView.get('horizontalScrollOffset'), 0, "A now-three-touch vertical scroll gesture should not scroll horizontally"); equals(scrollView.get('verticalScrollOffset'), 200, "A now-three-touch vertical scroll gesture should successfully scroll vertically");
});
test(“Adding and removing touches while scaling.”, function() {
equals(scrollView.get('scale'), 1, "PRELIM: Horizontal offset starts at"); equals(scrollView.get('verticalScrollOffset'), 0, "PRELIM: Vertical offset starts at"); equals(scrollView.get('horizontalScrollOffset'), 0, "PRELIM: Horizontal offset starts at"); // Start touches. evt.touches = []; evt.changedTouches = [evt, evt2]; SC.Event.trigger(targetLayer, 'touchstart', evt); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 2, "Two touches should result in two touches"); // Pinch out to 2x to begin scaling. evt.touches = [evt, evt2]; evt.pageX = evt.pageY = 300; evt2.pageX = evt2.pageY = 700; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('scale'), 2, "A 2x pinch gesture should double the scroll's scale"); equals(scrollView.get('horizontalScrollOffset'), 500, "A centered pinch gesture should move the horizontal offset by half the content view's change in width"); equals(scrollView.get('verticalScrollOffset'), 500, "A centered pinch gesture should move the vertical offset by half the content view's change in height"); // Remove our second touch. evt2.touches = [evt, evt2]; evt2.changedTouches = [evt2]; SC.Event.trigger(targetLayer, 'touchend', evt2); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 1, "Removing one of two touches should leave one touch"); equals(scrollView.get('scale'), 2, "Removing a touch shouldn't change scale"); equals(scrollView.get('horizontalScrollOffset'), 500, "Removing a touch shouldn't change the horizontal offset"); equals(scrollView.get('verticalScrollOffset'), 500, "Removing a touch shouldn't change the vertical offset"); // Add third touch to spot second touch just left. evt3.touches = [evt]; evt3.changedTouches = [evt3]; evt3.pageX = 700; evt3.pageY = 700; SC.Event.trigger(targetLayer, 'touchstart', evt3); equals(SC.RootResponder.responder.touchesForView(scrollView).length, 2, "Adding one touch to one touch should result in two touches"); equals(scrollView.get('scale'), 2, "Adding a touch shouldn't change scale"); equals(scrollView.get('horizontalScrollOffset'), 500, "Adding a touch shouldn't change the horizontal offset"); equals(scrollView.get('verticalScrollOffset'), 500, "Adding a touch shouldn't change the vertical offset"); // Pinch back to 1x. Should revert everybody to initial values. evt.touches = [evt, evt3]; evt.changedTouches = [evt, evt3]; evt.pageX = evt.pageY = 400; evt3.pageX = evt3.pageY = 600; SC.Event.trigger(targetLayer, 'touchmove', evt); equals(scrollView.get('scale'), 1, "Pinching back down by half should reverse doubling of scaling"); equals(scrollView.get('horizontalScrollOffset'), 0, "Pinching back down by half should reverse horizontal offset change"); equals(scrollView.get('verticalScrollOffset'), 0, "Pinching back down by half should reverse vertical offset change");
});