1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "testing/gtest/include/gtest/gtest.h"
6#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
7
8#include "base/mac/scoped_nsobject.h"
9#import "base/mac/sdk_forward_declarations.h"
10#import "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_history_swiper.h"
11#import "third_party/ocmock/OCMock/OCMock.h"
12#import "third_party/ocmock/ocmock_extensions.h"
13#include "third_party/WebKit/public/web/WebInputEvent.h"
14
15@interface HistorySwiper (MacHistorySwiperTest)
16- (BOOL)systemSettingsAllowHistorySwiping:(NSEvent*)event;
17- (BOOL)browserCanNavigateInDirection:
18        (history_swiper::NavigationDirection)forward
19                                event:(NSEvent*)event;
20- (void)endHistorySwipe;
21- (void)beginHistorySwipeInDirection:
22        (history_swiper::NavigationDirection)goForward
23                               event:(NSEvent*)event;
24- (void)navigateBrowserInDirection:(history_swiper::NavigationDirection)forward;
25- (void)initiateMagicMouseHistorySwipe:(BOOL)isRightScroll
26                                 event:(NSEvent*)event;
27@end
28
29class MacHistorySwiperTest : public CocoaTest {
30 public:
31  virtual void SetUp() OVERRIDE {
32    CocoaTest::SetUp();
33
34    [HistorySwiper resetMagicMouseState];
35
36    view_ = [[NSView alloc] init];
37    id mockDelegate =
38        [OCMockObject mockForProtocol:@protocol(HistorySwiperDelegate)];
39    [[[mockDelegate stub] andReturn:view_] viewThatWantsHistoryOverlay];
40    [[[mockDelegate stub] andReturnBool:YES] shouldAllowHistorySwiping];
41
42    base::scoped_nsobject<HistorySwiper> historySwiper(
43        [[HistorySwiper alloc] initWithDelegate:mockDelegate]);
44    id mockHistorySwiper = [OCMockObject partialMockForObject:historySwiper];
45    [[[mockHistorySwiper stub] andReturnBool:YES]
46        systemSettingsAllowHistorySwiping:[OCMArg any]];
47    [[[mockHistorySwiper stub] andReturnBool:YES]
48        browserCanNavigateInDirection:history_swiper::kForwards
49                                event:[OCMArg any]];
50    [[[mockHistorySwiper stub] andReturnBool:YES]
51        browserCanNavigateInDirection:history_swiper::kBackwards
52                                event:[OCMArg any]];
53    [[[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
54      ++begin_count_;
55      // beginHistorySwipeInDirection: calls endHistorySwipe internally.
56      --end_count_;
57    }] andForwardToRealObject]
58        beginHistorySwipeInDirection:history_swiper::kForwards
59                               event:[OCMArg any]];
60    [[[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
61      ++begin_count_;
62      // beginHistorySwipeInDirection: calls endHistorySwipe internally.
63      --end_count_;
64    }] andForwardToRealObject]
65        beginHistorySwipeInDirection:history_swiper::kBackwards
66                               event:[OCMArg any]];
67    [[[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
68      ++end_count_;
69    }] andForwardToRealObject] endHistorySwipe];
70    [[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
71        navigated_right_ = true;
72    }] navigateBrowserInDirection:history_swiper::kForwards];
73    [[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
74        navigated_left_ = true;
75    }] navigateBrowserInDirection:history_swiper::kBackwards];
76
77    [[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
78        magic_mouse_history_swipe_ = true;
79    }] initiateMagicMouseHistorySwipe:YES event:[OCMArg any]];
80    [[[mockHistorySwiper stub] andDo:^(NSInvocation* invocation) {
81        magic_mouse_history_swipe_ = true;
82    }] initiateMagicMouseHistorySwipe:NO event:[OCMArg any]];
83
84    historySwiper_ = [mockHistorySwiper retain];
85
86    begin_count_ = 0;
87    end_count_ = 0;
88    navigated_right_ = false;
89    navigated_left_ = false;
90    magic_mouse_history_swipe_ = false;
91  }
92
93  virtual void TearDown() OVERRIDE {
94    [view_ release];
95    [historySwiper_ release];
96    CocoaTest::TearDown();
97  }
98
99  void startGestureInMiddle();
100  void moveGestureInMiddle();
101  void moveGestureAtPoint(NSPoint point);
102  void momentumMoveGestureAtPoint(NSPoint point);
103  void endGestureAtPoint(NSPoint point);
104  void rendererACKForBeganEvent();
105
106  HistorySwiper* historySwiper_;
107  NSView* view_;
108  int begin_count_;
109  int end_count_;
110  bool navigated_right_;
111  bool navigated_left_;
112  bool magic_mouse_history_swipe_;
113};
114
115NSPoint makePoint(CGFloat x, CGFloat y) {
116  NSPoint point;
117  point.x = x;
118  point.y = y;
119  return point;
120}
121
122id mockEventWithPoint(NSPoint point, NSEventType type) {
123  id mockEvent = [OCMockObject mockForClass:[NSEvent class]];
124  id mockTouch = [OCMockObject mockForClass:[NSTouch class]];
125  [[[mockTouch stub] andReturnNSPoint:point] normalizedPosition];
126  NSArray* touches = @[mockTouch];
127  [[[mockEvent stub] andReturn:touches] touchesMatchingPhase:NSTouchPhaseAny
128    inView:[OCMArg any]];
129  [[[mockEvent stub] andReturnBool:NO] isDirectionInvertedFromDevice];
130  [(NSEvent*)[[mockEvent stub] andReturnValue:OCMOCK_VALUE(type)] type];
131
132  return mockEvent;
133}
134
135id scrollWheelEventWithPhase(NSEventPhase phase,
136                             NSEventPhase momentumPhase,
137                             CGFloat scrollingDeltaX,
138                             CGFloat scrollingDeltaY) {
139  // The point isn't used, so we pass in bogus data.
140  id event = mockEventWithPoint(makePoint(0,0), NSScrollWheel);
141  [(NSEvent*)[[event stub] andReturnValue:OCMOCK_VALUE(phase)] phase];
142  [(NSEvent*)
143      [[event stub] andReturnValue:OCMOCK_VALUE(momentumPhase)] momentumPhase];
144  [(NSEvent*)[[event stub]
145       andReturnValue:OCMOCK_VALUE(scrollingDeltaX)] scrollingDeltaX];
146  [(NSEvent*)[[event stub]
147       andReturnValue:OCMOCK_VALUE(scrollingDeltaY)] scrollingDeltaY];
148  return event;
149}
150
151id scrollWheelEventWithPhase(NSEventPhase phase,
152                             NSEventPhase momentumPhase) {
153  return scrollWheelEventWithPhase(phase, momentumPhase, 0, 0);
154}
155
156id scrollWheelEventWithPhase(NSEventPhase phase) {
157  return scrollWheelEventWithPhase(phase, NSEventPhaseNone);
158}
159
160void MacHistorySwiperTest::startGestureInMiddle() {
161  NSEvent* event = mockEventWithPoint(makePoint(0.5, 0.5), NSEventTypeGesture);
162  [historySwiper_ touchesBeganWithEvent:event];
163  [historySwiper_ beginGestureWithEvent:event];
164  NSEvent* scrollEvent = scrollWheelEventWithPhase(NSEventPhaseBegan);
165  [historySwiper_ handleEvent:scrollEvent];
166}
167
168void MacHistorySwiperTest::moveGestureInMiddle() {
169  moveGestureAtPoint(makePoint(0.5, 0.5));
170
171  // Callbacks from blink to set the relevant state for history swiping.
172  rendererACKForBeganEvent();
173}
174
175void MacHistorySwiperTest::moveGestureAtPoint(NSPoint point) {
176  NSEvent* event = mockEventWithPoint(point, NSEventTypeGesture);
177  [historySwiper_ touchesMovedWithEvent:event];
178
179  NSEvent* scrollEvent = scrollWheelEventWithPhase(NSEventPhaseChanged);
180  [historySwiper_ handleEvent:scrollEvent];
181}
182
183void MacHistorySwiperTest::momentumMoveGestureAtPoint(NSPoint point) {
184  NSEvent* event = mockEventWithPoint(point, NSEventTypeGesture);
185  [historySwiper_ touchesMovedWithEvent:event];
186
187  NSEvent* scrollEvent =
188      scrollWheelEventWithPhase(NSEventPhaseNone, NSEventPhaseChanged);
189  [historySwiper_ handleEvent:scrollEvent];
190}
191
192void MacHistorySwiperTest::endGestureAtPoint(NSPoint point) {
193  NSEvent* event = mockEventWithPoint(point, NSEventTypeGesture);
194  [historySwiper_ touchesEndedWithEvent:event];
195
196  NSEvent* scrollEvent = scrollWheelEventWithPhase(NSEventPhaseEnded);
197  [historySwiper_ handleEvent:scrollEvent];
198}
199
200void MacHistorySwiperTest::rendererACKForBeganEvent() {
201  blink::WebMouseWheelEvent event;
202  event.phase = blink::WebMouseWheelEvent::PhaseBegan;
203  [historySwiper_ rendererHandledWheelEvent:event consumed:NO];
204}
205
206// Test that a simple left-swipe causes navigation.
207TEST_F(MacHistorySwiperTest, SwipeLeft) {
208  // These tests require 10.7+ APIs.
209  if (![NSEvent
210          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
211    return;
212
213  startGestureInMiddle();
214  moveGestureInMiddle();
215
216  EXPECT_EQ(begin_count_, 0);
217  EXPECT_EQ(end_count_, 0);
218
219  moveGestureAtPoint(makePoint(0.2, 0.5));
220  EXPECT_EQ(begin_count_, 1);
221  EXPECT_EQ(end_count_, 0);
222  EXPECT_FALSE(navigated_right_);
223  EXPECT_FALSE(navigated_left_);
224
225  endGestureAtPoint(makePoint(0.2, 0.5));
226  EXPECT_EQ(begin_count_, 1);
227  EXPECT_EQ(end_count_, 1);
228  EXPECT_FALSE(navigated_right_);
229  EXPECT_TRUE(navigated_left_);
230}
231
232// Test that a simple right-swipe causes navigation.
233TEST_F(MacHistorySwiperTest, SwipeRight) {
234  // These tests require 10.7+ APIs.
235  if (![NSEvent
236          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
237    return;
238
239  startGestureInMiddle();
240  moveGestureInMiddle();
241
242  EXPECT_EQ(begin_count_, 0);
243  EXPECT_EQ(end_count_, 0);
244
245  moveGestureAtPoint(makePoint(0.8, 0.5));
246  EXPECT_EQ(begin_count_, 1);
247  EXPECT_EQ(end_count_, 0);
248  EXPECT_FALSE(navigated_right_);
249  EXPECT_FALSE(navigated_left_);
250
251  endGestureAtPoint(makePoint(0.8, 0.5));
252  EXPECT_EQ(begin_count_, 1);
253  EXPECT_EQ(end_count_, 1);
254  EXPECT_TRUE(navigated_right_);
255  EXPECT_FALSE(navigated_left_);
256}
257
258// If the user doesn't swipe enough, the history swiper should begin, but the
259// browser should not navigate.
260TEST_F(MacHistorySwiperTest, SwipeLeftSmallAmount) {
261  // These tests require 10.7+ APIs.
262  if (![NSEvent
263          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
264    return;
265
266  startGestureInMiddle();
267  moveGestureInMiddle();
268  moveGestureAtPoint(makePoint(0.45, 0.5));
269  endGestureAtPoint(makePoint(0.45, 0.5));
270  EXPECT_EQ(begin_count_, 1);
271  EXPECT_EQ(end_count_, 1);
272  EXPECT_FALSE(navigated_right_);
273  EXPECT_FALSE(navigated_left_);
274}
275
276// Diagonal swipes with a slight horizontal bias should not start the history
277// swiper.
278TEST_F(MacHistorySwiperTest, SwipeDiagonal) {
279  // These tests require 10.7+ APIs.
280  if (![NSEvent
281          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
282    return;
283
284  startGestureInMiddle();
285  moveGestureInMiddle();
286  moveGestureInMiddle();
287  moveGestureAtPoint(makePoint(0.6, 0.59));
288  endGestureAtPoint(makePoint(0.6, 0.59));
289
290  EXPECT_EQ(begin_count_, 1);
291  EXPECT_EQ(end_count_, 1);
292  EXPECT_FALSE(navigated_right_);
293  EXPECT_FALSE(navigated_left_);
294}
295
296// Swiping left and then down should cancel the history swiper without
297// navigating.
298TEST_F(MacHistorySwiperTest, SwipeLeftThenDown) {
299  // These tests require 10.7+ APIs.
300  if (![NSEvent
301          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
302    return;
303
304  startGestureInMiddle();
305  moveGestureInMiddle();
306  moveGestureAtPoint(makePoint(0.4, 0.5));
307  moveGestureAtPoint(makePoint(0.4, 0.3));
308  endGestureAtPoint(makePoint(0.2, 0.2));
309  EXPECT_EQ(begin_count_, 1);
310  EXPECT_EQ(end_count_, 1);
311  EXPECT_FALSE(navigated_right_);
312  EXPECT_FALSE(navigated_left_);
313}
314
315// Sometimes Cocoa gets confused and sends us a momentum swipe event instead of
316// a swipe gesture event. Momentum events should not cause history swiping.
317TEST_F(MacHistorySwiperTest, MomentumSwipeLeft) {
318  // These tests require 10.7+ APIs.
319  if (![NSEvent
320          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
321    return;
322
323  startGestureInMiddle();
324
325  // Send a momentum move gesture.
326  momentumMoveGestureAtPoint(makePoint(0.5, 0.5));
327  EXPECT_EQ(begin_count_, 0);
328  EXPECT_EQ(end_count_, 0);
329
330  // Callbacks from blink to set the relevant state for history swiping.
331  rendererACKForBeganEvent();
332
333  momentumMoveGestureAtPoint(makePoint(0.2, 0.5));
334  EXPECT_EQ(begin_count_, 0);
335  EXPECT_EQ(end_count_, 0);
336
337  endGestureAtPoint(makePoint(0.2, 0.5));
338  EXPECT_EQ(begin_count_, 0);
339  EXPECT_EQ(end_count_, 0);
340}
341
342// Momentum scroll events for magic mouse should not attempt to trigger the
343// `trackSwipeEventWithOptions:` api, as that throws an exception.
344TEST_F(MacHistorySwiperTest, MagicMouseMomentumSwipe) {
345  // These tests require 10.7+ APIs.
346  if (![NSEvent
347          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
348    return;
349
350  // Magic mouse events don't generate 'touches*' callbacks.
351  NSEvent* event = mockEventWithPoint(makePoint(0.5, 0.5), NSEventTypeGesture);
352  [historySwiper_ beginGestureWithEvent:event];
353  NSEvent* scrollEvent = scrollWheelEventWithPhase(NSEventPhaseBegan);
354  [historySwiper_ handleEvent:scrollEvent];
355
356  // Callbacks from blink to set the relevant state for history swiping.
357  rendererACKForBeganEvent();
358
359  // Send a momentum move gesture.
360  scrollEvent =
361      scrollWheelEventWithPhase(NSEventPhaseNone, NSEventPhaseChanged, 5.0, 0);
362  [historySwiper_ handleEvent:scrollEvent];
363
364  EXPECT_FALSE(magic_mouse_history_swipe_);
365}
366
367// User starts a swipe but doesn't move.
368TEST_F(MacHistorySwiperTest, NoSwipe) {
369  // These tests require 10.7+ APIs.
370  if (![NSEvent
371          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
372    return;
373
374  startGestureInMiddle();
375  moveGestureInMiddle();
376
377  // Starts the gesture.
378  moveGestureAtPoint(makePoint(0.44, 0.44));
379
380  // No movement.
381  endGestureAtPoint(makePoint(0.44, 0.44));
382
383  EXPECT_EQ(begin_count_, 1);
384  EXPECT_EQ(end_count_, 1);
385  EXPECT_FALSE(navigated_right_);
386  EXPECT_FALSE(navigated_left_);
387}
388
389// After a gesture is successfully recognized, momentum events should be
390// swallowed, but new events should pass through.
391TEST_F(MacHistorySwiperTest, TouchEventAfterGestureFinishes) {
392  // These tests require 10.7+ APIs.
393  if (![NSEvent
394          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
395    return;
396
397  // Successfully pass through a gesture.
398  startGestureInMiddle();
399  moveGestureInMiddle();
400  moveGestureAtPoint(makePoint(0.8, 0.5));
401  endGestureAtPoint(makePoint(0.8, 0.5));
402  EXPECT_TRUE(navigated_right_);
403
404  // Momentum events should be swallowed.
405  NSEvent* momentumEvent = scrollWheelEventWithPhase(NSEventPhaseNone,
406                                                     NSEventPhaseChanged);
407  EXPECT_TRUE([historySwiper_ handleEvent:momentumEvent]);
408
409  // New events should not be swallowed.
410  NSEvent* beganEvent = scrollWheelEventWithPhase(NSEventPhaseBegan);
411  EXPECT_FALSE([historySwiper_ handleEvent:beganEvent]);
412}
413
414// The history swipe logic should be resilient against the timing of the
415// different callbacks that result from scrolling.
416TEST_F(MacHistorySwiperTest, SwipeRightEventOrdering) {
417  // These tests require 10.7+ APIs.
418  if (![NSEvent
419          respondsToSelector:@selector(isSwipeTrackingFromScrollEventsEnabled)])
420    return;
421
422  // Touches began.
423  NSEvent* scrollEvent = scrollWheelEventWithPhase(NSEventPhaseBegan);
424  NSEvent* event = mockEventWithPoint(makePoint(0.5, 0.5), NSEventTypeGesture);
425  [historySwiper_ touchesBeganWithEvent:event];
426  [historySwiper_ handleEvent:scrollEvent];
427  rendererACKForBeganEvent();
428
429  // Touches moved.
430  moveGestureAtPoint(makePoint(0.5, 0.5));
431  EXPECT_EQ(begin_count_, 0);
432  EXPECT_EQ(end_count_, 0);
433
434  // Touches moved.
435  moveGestureAtPoint(makePoint(0.52, 0.5));
436
437  // Begin gesture callback is delayed.
438  [historySwiper_ beginGestureWithEvent:event];
439
440  // Touches moved.
441  moveGestureAtPoint(makePoint(0.52, 0.5));
442
443  // Complete the rest of the gesture.
444  moveGestureAtPoint(makePoint(0.60, 0.5));
445  scrollEvent = scrollWheelEventWithPhase(NSEventPhaseChanged);
446  [historySwiper_ handleEvent:scrollEvent];
447  endGestureAtPoint(makePoint(0.70, 0.5));
448
449  EXPECT_EQ(begin_count_, 1);
450  EXPECT_EQ(end_count_, 1);
451  EXPECT_TRUE(navigated_right_);
452  EXPECT_FALSE(navigated_left_);
453}
454