| /* |
| * Copyright 2011 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.touch.client; |
| |
| import com.google.gwt.core.client.Duration; |
| import com.google.gwt.core.client.Scheduler.RepeatingCommand; |
| import com.google.gwt.dom.client.NativeEvent; |
| import com.google.gwt.dom.client.Touch; |
| import com.google.gwt.event.dom.client.TouchEvent; |
| import com.google.gwt.event.dom.client.TouchStartEvent; |
| import com.google.gwt.junit.client.GWTTestCase; |
| import com.google.gwt.touch.client.TouchScroller.TemporalPoint; |
| import com.google.gwt.user.client.ui.HasScrolling; |
| import com.google.gwt.user.client.ui.Label; |
| import com.google.gwt.user.client.ui.RootPanel; |
| import com.google.gwt.user.client.ui.ScrollPanel; |
| |
| /** |
| * Tests for {@link TouchScroller}. |
| * |
| * <p> |
| * Many of the tests in this class can run even in HtmlUnit and browsers that do |
| * not support touch events because we create mock touch events. |
| * </p> |
| */ |
| public class TouchScrollTest extends GWTTestCase { |
| |
| /** |
| * A custom {@link ScrollPanel} that doesn't rely on the DOM to calculate its |
| * vertical and horizontal position. Allows testing in HtmlUnit. |
| */ |
| private static class CustomScrollPanel extends ScrollPanel { |
| private final int maxHorizontalScrollPosition; |
| private final int maxVerticalScrollPosition; |
| private final int minHorizontalScrollPosition; |
| private final int minVerticalScrollPosition; |
| private int horizontalScrollPosition; |
| private int verticalScrollPosition; |
| |
| /** |
| * Construct a new {@link CustomScrollPanel} using 0 as the minimum vertical |
| * and horizontal scroll position and INTEGER.MAX_VALUE as the maximum |
| * positions. |
| */ |
| public CustomScrollPanel() { |
| this.minVerticalScrollPosition = 0; |
| this.maxVerticalScrollPosition = 5000; |
| this.minHorizontalScrollPosition = 0; |
| this.maxHorizontalScrollPosition = 5000; |
| } |
| |
| @Override |
| public int getHorizontalScrollPosition() { |
| return horizontalScrollPosition; |
| } |
| |
| @Override |
| public int getMaximumHorizontalScrollPosition() { |
| return maxHorizontalScrollPosition; |
| } |
| |
| @Override |
| public int getMaximumVerticalScrollPosition() { |
| return maxVerticalScrollPosition; |
| } |
| |
| @Override |
| public int getMinimumHorizontalScrollPosition() { |
| return minHorizontalScrollPosition; |
| } |
| |
| @Override |
| public int getMinimumVerticalScrollPosition() { |
| return minVerticalScrollPosition; |
| } |
| |
| @Override |
| public int getVerticalScrollPosition() { |
| return verticalScrollPosition; |
| } |
| |
| @Override |
| public void setHorizontalScrollPosition(int position) { |
| this.horizontalScrollPosition = position; |
| super.setHorizontalScrollPosition(position); |
| } |
| |
| @Override |
| public void setVerticalScrollPosition(int position) { |
| this.verticalScrollPosition = position; |
| super.setVerticalScrollPosition(position); |
| } |
| } |
| |
| /** |
| * A custom touch event. |
| */ |
| private static class CustomTouchEvent extends TouchStartEvent { |
| } |
| |
| /** |
| * A {@link TouchScroller} that overrides drag events. |
| */ |
| private static class CustomTouchScroller extends TouchScroller { |
| |
| private boolean setupBustClickHandlerCalled; |
| private boolean removeBustClickHandlerCalled; |
| private boolean removeAttachHandlerCalled; |
| private boolean onDragEndCalled; |
| private boolean onDragMoveCalled; |
| private boolean onDragStartCalled; |
| |
| public CustomTouchScroller(HasScrolling widget) { |
| super(); |
| setTargetWidget(widget); |
| } |
| |
| public void assertOnDragEndCalled(boolean expected) { |
| assertEquals(expected, onDragEndCalled); |
| onDragEndCalled = false; |
| } |
| |
| public void assertOnDragMoveCalled(boolean expected) { |
| assertEquals(expected, onDragMoveCalled); |
| onDragMoveCalled = false; |
| } |
| |
| public void assertOnDragStartCalled(boolean expected) { |
| assertEquals(expected, onDragStartCalled); |
| onDragStartCalled = false; |
| } |
| |
| public void assertSetupBustClickHandlerCalled(boolean expected) { |
| assertEquals(expected, setupBustClickHandlerCalled); |
| setupBustClickHandlerCalled = false; |
| } |
| |
| public void assertRemoveBustClickHandlerCalled(boolean expected) { |
| assertEquals(expected, removeBustClickHandlerCalled); |
| removeBustClickHandlerCalled = false; |
| } |
| |
| public void assertRemoveAttachHandlerCalled(boolean expected) { |
| assertEquals(expected, removeAttachHandlerCalled); |
| removeAttachHandlerCalled = false; |
| } |
| |
| @Override |
| protected void onDragEnd(TouchEvent<?> event) { |
| assertFalse("onDragEnd called twice", onDragEndCalled); |
| super.onDragEnd(event); |
| onDragEndCalled = true; |
| } |
| |
| @Override |
| protected void onDragMove(TouchEvent<?> event) { |
| assertFalse("onDragMove called twice", onDragMoveCalled); |
| super.onDragMove(event); |
| onDragMoveCalled = true; |
| } |
| |
| @Override |
| protected void onDragStart(TouchEvent<?> event) { |
| assertFalse("onDragStart called twice", onDragStartCalled); |
| super.onDragStart(event); |
| onDragStartCalled = true; |
| } |
| |
| @Override |
| protected void setupBustClickHandler() { |
| super.setupBustClickHandler(); |
| setupBustClickHandlerCalled = true; |
| } |
| |
| @Override |
| protected void removeBustClickHandler() { |
| super.removeBustClickHandler(); |
| removeBustClickHandlerCalled = true; |
| } |
| |
| @Override |
| protected void removeAttachHandler() { |
| super.removeAttachHandler(); |
| removeAttachHandlerCalled = true; |
| } |
| } |
| |
| /** |
| * Create a mock native touch event that contains no touches. |
| * |
| * @return an empty mock touch event |
| */ |
| private static native NativeEvent createNativeTouchEvent() /*-{ |
| // Create a real event so standard event methods are available. |
| var touches = []; |
| return { |
| "changedTouches" : touches, |
| "targetTouches" : touches, |
| "touches" : touches, |
| "preventDefault" : function() {} // Called by TouchScroller. |
| }; |
| }-*/; |
| |
| /** |
| * Create a mock {@link Touch} for the specified x and y coordinate. |
| * |
| * @param x the x coordinate |
| * @param y the y coordinate |
| * @return a mock touch |
| */ |
| private static native Touch createTouch(int x, int y) /*-{ |
| return { |
| "clientX" : x, |
| "clientY" : y, |
| "identifier" : 0, |
| "pageX" : x, |
| "pageY" : y, |
| "screenX" : x, |
| "screenY" : y, |
| "target" : null |
| }; |
| }-*/; |
| |
| /** |
| * Create a mock TouchEndEvent. Touch end events do not have any touches. |
| * |
| * @return a mock TouchEndEvent |
| */ |
| private static TouchEvent<?> createTouchEndEvent() { |
| CustomTouchEvent event = new CustomTouchEvent(); |
| event.setNativeEvent(createNativeTouchEvent()); |
| return event; |
| } |
| |
| /** |
| * Create a mock TouchMoveEvent for the specified x and y coordinate. |
| * |
| * @param x the x coordinate |
| * @param y the y coordinate |
| * @return a mock TouchMoveEvent |
| */ |
| private static TouchEvent<?> createTouchMoveEvent(int x, int y) { |
| // TouchScroller doesn't care about the actual event subclass. |
| return createTouchStartEvent(x, y); |
| } |
| |
| /** |
| * Create a mock {@link TouchStartEvent} for the specified x and y coordinate. |
| * |
| * @param x the x coordinate |
| * @param y the y coordinate |
| * @return a mock {@link TouchStartEvent} |
| */ |
| private static TouchEvent<?> createTouchStartEvent(int x, int y) { |
| CustomTouchEvent event = new CustomTouchEvent(); |
| NativeEvent nativeEvent = createNativeTouchEvent(); |
| nativeEvent.getTouches().push(createTouch(x, y)); |
| event.setNativeEvent(nativeEvent); |
| return event; |
| } |
| |
| private CustomTouchScroller scroller; |
| private CustomScrollPanel scrollPanel; |
| |
| @Override |
| public String getModuleName() { |
| return "com.google.gwt.touch.Touch"; |
| } |
| |
| public void testCalculateEndVlocity() { |
| // Two points at the same time should return null. |
| TemporalPoint from = new TemporalPoint(new Point(100.0, 200.0), 0); |
| TemporalPoint sameTime = new TemporalPoint(new Point(100.0, 100.0), 0); |
| assertNull(scroller.calculateEndVelocity(from, sameTime)); |
| |
| // Two different points should return a velocity. |
| TemporalPoint to = new TemporalPoint(new Point(250.0, 150.0), 25); |
| assertEquals(new Point(-6.0, 2.0), scroller.calculateEndVelocity(from, to)); |
| } |
| |
| public void testCreateIfSupported() { |
| // createIfSupported() |
| TouchScroller scroller = TouchScroller.createIfSupported(); |
| if (TouchScroller.isSupported()) { |
| assertNotNull("TouchScroll not created, but touch is supported", scroller); |
| assertNull(scroller.getTargetWidget()); |
| |
| } else { |
| assertNull("TouchScroll created, but touch is not supported", scroller); |
| } |
| |
| // createIfSupported(HasScrolling) |
| HasScrolling target = new ScrollPanel(); |
| scroller = TouchScroller.createIfSupported(target); |
| if (TouchScroller.isSupported()) { |
| assertNotNull("TouchScroll not created, but touch is supported", scroller); |
| assertEquals(target, scroller.getTargetWidget()); |
| |
| } else { |
| assertNull("TouchScroll created, but touch is not supported", scroller); |
| } |
| } |
| |
| public void testDeferToNativeScrollingBottom() { |
| testDeferToNativeScrolling(0, scrollPanel.getMaximumVerticalScrollPosition(), 0, -100); |
| } |
| |
| public void testDeferToNativeScrollingLeft() { |
| testDeferToNativeScrolling(0, 0, 100, 0); |
| } |
| |
| public void testDeferToNativeScrollingRight() { |
| testDeferToNativeScrolling(scrollPanel.getMaximumHorizontalScrollPosition(), 0, -100, 0); |
| } |
| |
| public void testDeferToNativeScrollingTop() { |
| testDeferToNativeScrolling(0, 0, 0, 100); |
| } |
| |
| /** |
| * Test that touch events correctly initiate drag events. |
| */ |
| public void testDragSequence() { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Initial state. |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Start touching. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| scroller.assertOnDragStartCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Move, but not enough to drag. |
| scroller.onTouchMove(createTouchMoveEvent(-1, 0)); |
| scroller.assertOnDragStartCalled(false); |
| scroller.assertOnDragMoveCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Move. |
| scroller.onTouchMove(createTouchMoveEvent(-100, 0)); |
| scroller.assertOnDragStartCalled(true); |
| scroller.assertOnDragMoveCalled(true); |
| assertTrue(scroller.isTouching()); |
| assertTrue(scroller.isDragging()); |
| |
| // Move again. |
| scroller.onTouchMove(createTouchMoveEvent(-200, 0)); |
| scroller.assertOnDragStartCalled(false); // drag already started. |
| scroller.assertOnDragMoveCalled(true); |
| assertTrue(scroller.isTouching()); |
| assertTrue(scroller.isDragging()); |
| |
| // End. |
| scroller.onTouchEnd(createTouchEndEvent()); |
| scroller.assertOnDragStartCalled(false); |
| scroller.assertOnDragMoveCalled(false); |
| scroller.assertOnDragEndCalled(true); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| } |
| |
| /** |
| * Test that the bust click/attach event handler is removed from the old |
| * widget. |
| */ |
| public void testHandlersRemovedFromOldWidget() { |
| CustomScrollPanel newScrollPanel = new CustomScrollPanel(); |
| |
| // Initial state. |
| scroller.assertRemoveBustClickHandlerCalled(true); |
| scroller.assertRemoveAttachHandlerCalled(true); |
| |
| // Replace the old widget (scrollPanel) with the new widget (newScrollPanel) |
| scroller.setTargetWidget(newScrollPanel); |
| |
| // Verify that the bust click handler and attach event handler are removed. |
| scroller.assertRemoveBustClickHandlerCalled(true); |
| scroller.assertRemoveAttachHandlerCalled(true); |
| |
| // Remove the old widget (scrollPanel) from the root panel. |
| RootPanel.get().remove(scrollPanel); |
| |
| // Verify that removing the old widget doesn't cause removeBustClickHandler |
| // from being called. |
| scroller.assertRemoveBustClickHandlerCalled(false); |
| } |
| |
| /** |
| * Test that when momentum ends, the momentum command is set to null (and |
| * isMomentumActive() returns false). |
| */ |
| public void testMomentumEnd() { |
| // Use a short lived momentum. |
| scroller.setMomentum(new DefaultMomentum() { |
| @Override |
| public boolean updateState(State state) { |
| // Immediately end momentum. |
| return false; |
| } |
| }); |
| |
| // Start a drag sequence. |
| double millis = Duration.currentTimeMillis(); |
| scroller.getRecentTouchPosition().setTemporalPoint(new Point(0, 0), millis); |
| scroller.getLastTouchPosition().setTemporalPoint(new Point(100, 100), millis + 100); |
| |
| // End the drag sequence. |
| scroller.onDragEnd(createTouchEndEvent()); |
| scroller.assertOnDragEndCalled(true); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| assertTrue(scroller.isMomentumActive()); |
| |
| // Force momentum to run, which causes it to end. |
| getMomentumCommand(scroller).execute(); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| assertFalse(scroller.isMomentumActive()); |
| } |
| |
| public void testOnDragEnd() { |
| // Start a drag sequence. |
| double millis = Duration.currentTimeMillis(); |
| scroller.getRecentTouchPosition().setTemporalPoint(new Point(0, 0), millis); |
| scroller.getLastTouchPosition().setTemporalPoint(new Point(100, 100), millis + 100); |
| |
| // End the drag sequence. |
| scroller.onDragEnd(createTouchEndEvent()); |
| assertTrue(scroller.isMomentumActive()); |
| } |
| |
| public void testOnDragEndNoMomentum() { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Start a drag sequence. |
| double millis = Duration.currentTimeMillis(); |
| scroller.getRecentTouchPosition().setTemporalPoint(new Point(0, 0), millis); |
| scroller.getLastTouchPosition().setTemporalPoint(new Point(100, 100), millis + 100); |
| |
| // End the drag sequence. |
| scroller.onDragEnd(createTouchEndEvent()); |
| assertFalse(scroller.isMomentumActive()); |
| } |
| |
| public void testOnDragMove() { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Start at 100x100; |
| scrollPanel.setHorizontalScrollPosition(100); |
| scrollPanel.setVerticalScrollPosition(150); |
| |
| // Start touching. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| |
| // Drag in a positive direction (negative scroll). |
| TouchEvent<?> touchMove = createTouchMoveEvent(40, 50); |
| scroller.onTouchMove(touchMove); |
| scroller.assertOnDragMoveCalled(true); |
| assertEquals(60, scrollPanel.getHorizontalScrollPosition()); |
| assertEquals(100, scrollPanel.getVerticalScrollPosition()); |
| |
| // Drag in a negative direction (positive scroll). |
| touchMove = createTouchMoveEvent(-20, -30); |
| scroller.onTouchMove(touchMove); |
| scroller.assertOnDragMoveCalled(true); |
| assertEquals(120, scrollPanel.getHorizontalScrollPosition()); |
| assertEquals(180, scrollPanel.getVerticalScrollPosition()); |
| } |
| |
| /** |
| * Test that touch end events are ignored if not touching. |
| */ |
| public void testOnTouchEndIgnored() { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Initial state. |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Verify that an extraneous touch end event is ignored. |
| scroller.onTouchEnd(createTouchEndEvent()); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| } |
| |
| /** |
| * Test that we handle touch end events that occur without initiating a drag |
| * sequence. |
| */ |
| public void testOnTouchEndWithoutDrag() { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Initial state. |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Start touching. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| scroller.assertOnDragStartCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Move, but not enough to drag. |
| scroller.onTouchMove(createTouchMoveEvent(1, 0)); |
| scroller.assertOnDragStartCalled(false); |
| scroller.assertOnDragMoveCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // End. |
| scroller.onTouchEnd(createTouchEndEvent()); |
| scroller.assertOnDragStartCalled(false); |
| scroller.assertOnDragMoveCalled(false); |
| scroller.assertOnDragEndCalled(false); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| } |
| |
| /** |
| * Test that touch move events are ignored if not touching. |
| */ |
| public void testOnTouchMoveIgnored() { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Initial state. |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Verify that an extraneous touchmove event is ignored. |
| scroller.onTouchMove(createTouchMoveEvent(0, 0)); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| } |
| |
| /** |
| * Test that touch start events cancel any active momentum. |
| */ |
| public void testOnTouchCancelsMomentum() { |
| // Start momentum. |
| double millis = Duration.currentTimeMillis(); |
| scroller.getRecentTouchPosition().setTemporalPoint(new Point(0, 0), millis); |
| scroller.getLastTouchPosition().setTemporalPoint(new Point(100, 100), millis + 100); |
| scroller.onDragEnd(createTouchEndEvent()); |
| assertTrue(scroller.isMomentumActive()); |
| |
| // Touch again. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| assertFalse(scroller.isMomentumActive()); |
| } |
| |
| /** |
| * Test that touch start events are ignored if already touching. |
| */ |
| public void testOnTouchStartIgnored() { |
| scroller.setMomentum(null); // Disable momentum for this test. |
| |
| // Initial state. |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Start touching. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| scroller.assertOnDragStartCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Verify that additional start events do not cause errors. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| scroller.assertOnDragStartCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| } |
| |
| /** |
| * Test that the setupBustClickHandler is called when the widget is detached |
| * and re-attached. |
| */ |
| public void testSetupBustClickHandler() { |
| // Initial state. |
| scroller.assertRemoveBustClickHandlerCalled(true); |
| scroller.assertRemoveAttachHandlerCalled(true); |
| scroller.assertSetupBustClickHandlerCalled(true); |
| |
| RootPanel.get().remove(scrollPanel); |
| |
| // Verify that the bust click handler is removed. |
| scroller.assertRemoveBustClickHandlerCalled(true); |
| |
| RootPanel.get().add(scrollPanel); |
| |
| // Verify that the bust click handler is setup. |
| scroller.assertSetupBustClickHandlerCalled(true); |
| } |
| |
| @Override |
| protected void gwtSetUp() throws Exception { |
| // Create and attach a widget that has scrolling. |
| scrollPanel = new CustomScrollPanel(); |
| scrollPanel.setPixelSize(500, 500); |
| Label content = new Label("Content"); |
| content.setPixelSize(10000, 10000); |
| RootPanel.get().add(scrollPanel); |
| |
| // Disabled touch scrolling because we will add our own scroller. |
| scrollPanel.setTouchScrollingDisabled(true); |
| |
| // Add scrolling support. |
| scroller = new CustomTouchScroller(scrollPanel); |
| } |
| |
| /** |
| * A replacement for JUnit's {@link #tearDown()} method. This method runs once |
| * per test method in your subclass, just after your each test method runs and |
| * can be used to perform cleanup. Override this method instead of |
| * {@link #tearDown()}. This method is run even in pure Java mode (non-GWT). |
| * |
| * @see #setForcePureJava |
| */ |
| @Override |
| protected void gwtTearDown() throws Exception { |
| // Detach the widget. |
| RootPanel.get().remove(scrollPanel.asWidget()); |
| scrollPanel = null; |
| scroller = null; |
| } |
| |
| /** |
| * Get the momentum command from the specified {@link TouchScroller}. |
| */ |
| private native RepeatingCommand getMomentumCommand(TouchScroller scroller) /*-{ |
| return scroller.@com.google.gwt.touch.client.TouchScroller::momentumCommand; |
| }-*/; |
| |
| /** |
| * Test that {@link TouchScroller} defers to native scrolling if the |
| * scrollable widget is already scrolled as far as it can go. |
| * |
| * @param hStart the starting horizontal scroll position |
| * @param vStart the starting vertical scroll position |
| * @param xEnd the ending x touch coordinate |
| * @param yEnd the ending y touch coordinate |
| */ |
| private void testDeferToNativeScrolling(int hStart, int vStart, int xEnd, int yEnd) { |
| // Disable momentum for this test. |
| scroller.setMomentum(null); |
| |
| // Scroll to the left. |
| scrollPanel.setHorizontalScrollPosition(hStart); |
| scrollPanel.setVerticalScrollPosition(vStart); |
| |
| // Start touching. |
| scroller.onTouchStart(createTouchStartEvent(0, 0)); |
| scroller.assertOnDragStartCalled(false); |
| assertTrue(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| |
| // Move to the left. |
| scroller.onTouchMove(createTouchMoveEvent(xEnd, yEnd)); |
| scroller.assertOnDragStartCalled(false); |
| scroller.assertOnDragMoveCalled(false); |
| assertFalse(scroller.isTouching()); |
| assertFalse(scroller.isDragging()); |
| } |
| } |