/* * 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()); } }