1// Copyright 2014 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#import <Carbon/Carbon.h>
6#import <Cocoa/Cocoa.h>
7#include <stdint.h>
8
9#include <memory>
10#include <utility>
11
12#include "base/mac/scoped_cftyperef.h"
13#include "base/macros.h"
14#include "testing/gtest/include/gtest/gtest.h"
15#import "ui/base/test/cocoa_helper.h"
16#import "ui/events/cocoa/cocoa_event_utils.h"
17#include "ui/events/event_constants.h"
18#include "ui/events/event_utils.h"
19#import "ui/events/test/cocoa_test_event_utils.h"
20#include "ui/events/types/event_type.h"
21#include "ui/gfx/geometry/point.h"
22
23namespace ui {
24
25namespace {
26
27// Although CGEventFlags is just a typedef to int in 10.10 and earlier headers,
28// the 10.11 header makes this a CF_ENUM, but doesn't give an option for "none".
29const CGEventFlags kNoEventFlags = static_cast<CGEventFlags>(0);
30
31class EventsMacTest : public CocoaTest {
32 public:
33  EventsMacTest() {}
34
35  gfx::Point Flip(gfx::Point window_location) {
36    NSRect window_frame = [test_window() frame];
37    CGFloat content_height =
38        NSHeight([test_window() contentRectForFrameRect:window_frame]);
39    window_location.set_y(content_height - window_location.y());
40    return window_location;
41  }
42
43  // TODO(tapted): Move this to cocoa_test_event_utils. It's not a drop-in
44  // replacement because -[NSApp sendEvent:] may route events generated this way
45  // differently.
46  NSEvent* TestMouseEvent(CGEventType type,
47                          const gfx::Point& window_location,
48                          CGEventFlags event_flags) {
49    // CGEventCreateMouseEvent() ignores the CGMouseButton parameter unless
50    // |type| is one of kCGEventOtherMouse{Up,Down,Dragged}. It can be an
51    // integer up to 31. However, constants are only supplied up to 2. For now,
52    // just assume "other" means the third/center mouse button, and rely on
53    // Quartz ignoring it when the type is not "other".
54    CGMouseButton other_button = kCGMouseButtonCenter;
55    CGPoint screen_point = cocoa_test_event_utils::ScreenPointFromWindow(
56        Flip(window_location).ToCGPoint(), test_window());
57    base::ScopedCFTypeRef<CGEventRef> mouse(
58        CGEventCreateMouseEvent(nullptr, type, screen_point, other_button));
59    CGEventSetFlags(mouse, event_flags);
60    return cocoa_test_event_utils::AttachWindowToCGEvent(mouse, test_window());
61  }
62
63  // Creates a scroll event from a "real" mouse wheel (i.e. not a trackpad).
64  NSEvent* TestScrollEvent(const gfx::Point& window_location,
65                           int32_t delta_x,
66                           int32_t delta_y) {
67    bool precise = false;
68    return cocoa_test_event_utils::TestScrollEvent(
69        Flip(window_location).ToCGPoint(), test_window(), delta_x, delta_y,
70        precise, NSEventPhaseNone, NSEventPhaseNone);
71  }
72
73  // Creates the sequence of events generated by a trackpad scroll.
74  // |initial_rest| indicates whether there is a pause before scrolling starts.
75  // |delta_y| is the portion to scroll without momentum (fingers on the
76  // trackpad). |momentum_delta_y| is the momentum portion. A zero delta skips
77  // that phase (if both are zero, the |initial_rest| is cancelled).
78  NSArray* TrackpadScrollSequence(bool initial_rest,
79                                  int32_t delta_y,
80                                  int32_t momentum_delta_y);
81
82 protected:
83  const gfx::Point default_location_ = gfx::Point(10, 20);
84
85 private:
86  DISALLOW_COPY_AND_ASSIGN(EventsMacTest);
87};
88
89// Trackpad scroll sequences below determined empirically on OSX 10.11 (linking
90// to 10.10 SDK), and dumping out with NSLog in -[NSView scrollWheel:]. First
91// created using a Magic Trackpad 2 on a MacPro. See the Trackpad* test cases
92// below for example event streams.
93NSArray* EventsMacTest::TrackpadScrollSequence(bool initial_rest,
94                                               int32_t delta_y,
95                                               int32_t momentum_delta_y) {
96  int32_t delta_x = 0;  // Just test vertical scrolling for now.
97  base::scoped_nsobject<NSMutableArray> events([[NSMutableArray alloc] init]);
98
99  // Resting part.
100  if (initial_rest) {
101    // MayBegin always has a zero delta.
102    [events addObject:cocoa_test_event_utils::TestScrollEvent(
103        Flip(default_location_).ToCGPoint(), test_window(), delta_x, 0, true,
104        NSEventPhaseMayBegin, NSEventPhaseNone)];
105    if (delta_y == 0) {
106      // Rest and release: event gets cancelled.
107      DCHECK_EQ(0, momentum_delta_y);  // Pretty sure this is impossible.
108      [events addObject:cocoa_test_event_utils::TestScrollEvent(
109          Flip(default_location_).ToCGPoint(), test_window(), delta_x, 0, true,
110          NSEventPhaseCancelled, NSEventPhaseNone)];
111      return events.autorelease();
112    }
113  }
114
115  // With or without a rest, a begin is sent. It can have a non-zero
116  // deviceDeltaY but regular deltaY is always 0.
117  [events addObject:cocoa_test_event_utils::TestScrollEvent(
118      Flip(default_location_).ToCGPoint(), test_window(), delta_x, 0, true,
119      NSEventPhaseBegan, NSEventPhaseNone)];
120
121  [events addObject:cocoa_test_event_utils::TestScrollEvent(
122      Flip(default_location_).ToCGPoint(), test_window(), delta_x, delta_y,
123      true, NSEventPhaseChanged, NSEventPhaseNone)];
124
125  // With or without momentum, an end is sent for the non-momentum part.
126  [events addObject:cocoa_test_event_utils::TestScrollEvent(
127      Flip(default_location_).ToCGPoint(), test_window(), delta_x, 0, true,
128      NSEventPhaseEnded, NSEventPhaseNone)];
129
130  if (momentum_delta_y == 0)
131    return events.autorelease();
132
133  // Flick part. Basically the same, but with phase and momentumPhase swapped.
134  [events addObject:cocoa_test_event_utils::TestScrollEvent(
135      Flip(default_location_).ToCGPoint(), test_window(), delta_x, 0, true,
136      NSEventPhaseNone, NSEventPhaseBegan)];
137
138  [events addObject:cocoa_test_event_utils::TestScrollEvent(
139      Flip(default_location_).ToCGPoint(), test_window(), delta_x,
140      momentum_delta_y, true, NSEventPhaseNone, NSEventPhaseChanged)];
141
142  [events addObject:cocoa_test_event_utils::TestScrollEvent(
143      Flip(default_location_).ToCGPoint(), test_window(), delta_x, 0, true,
144      NSEventPhaseNone, NSEventPhaseEnded)];
145  return events.autorelease();
146}
147
148}  // namespace
149
150TEST_F(EventsMacTest, EventFlagsFromNative) {
151  // Left click.
152  NSEvent* left = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0);
153  EXPECT_EQ(EF_LEFT_MOUSE_BUTTON, EventFlagsFromNative(left));
154
155  // Right click.
156  NSEvent* right = cocoa_test_event_utils::MouseEventWithType(NSRightMouseUp,
157                                                              0);
158  EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON, EventFlagsFromNative(right));
159
160  // Middle click.
161  NSEvent* middle = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseUp,
162                                                               0);
163  EXPECT_EQ(EF_MIDDLE_MOUSE_BUTTON, EventFlagsFromNative(middle));
164
165  // Caps + Left
166  NSEvent* caps = cocoa_test_event_utils::MouseEventWithType(
167      NSLeftMouseUp, NSAlphaShiftKeyMask);
168  EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_CAPS_LOCK_ON, EventFlagsFromNative(caps));
169
170  // Shift + Left
171  NSEvent* shift = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
172                                                              NSShiftKeyMask);
173  EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_SHIFT_DOWN, EventFlagsFromNative(shift));
174
175  // Ctrl + Left. Note we map this to a right click on Mac and remove Control.
176  NSEvent* ctrl = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
177                                                             NSControlKeyMask);
178  EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON, EventFlagsFromNative(ctrl));
179
180  // Ctrl + Right. Remains a right click.
181  NSEvent* ctrl_right = cocoa_test_event_utils::MouseEventWithType(
182      NSRightMouseUp, NSControlKeyMask);
183  EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON | EF_CONTROL_DOWN,
184            EventFlagsFromNative(ctrl_right));
185
186  // Alt + Left
187  NSEvent* alt = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
188                                                            NSAlternateKeyMask);
189  EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_ALT_DOWN, EventFlagsFromNative(alt));
190
191  // Cmd + Left
192  NSEvent* cmd = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp,
193                                                            NSCommandKeyMask);
194  EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN, EventFlagsFromNative(cmd));
195
196  // Shift + Ctrl + Left. Also mapped to a right-click. Control removed.
197  NSEvent* shiftctrl = cocoa_test_event_utils::MouseEventWithType(
198      NSLeftMouseUp, NSShiftKeyMask | NSControlKeyMask);
199  EXPECT_EQ(EF_RIGHT_MOUSE_BUTTON | EF_SHIFT_DOWN,
200            EventFlagsFromNative(shiftctrl));
201
202  // Cmd + Alt + Right
203  NSEvent* cmdalt = cocoa_test_event_utils::MouseEventWithType(
204      NSLeftMouseUp, NSCommandKeyMask | NSAlternateKeyMask);
205  EXPECT_EQ(EF_LEFT_MOUSE_BUTTON | EF_COMMAND_DOWN | EF_ALT_DOWN,
206            EventFlagsFromNative(cmdalt));
207}
208
209// Tests mouse button presses and mouse wheel events.
210TEST_F(EventsMacTest, ButtonEvents) {
211  gfx::Point location(5, 10);
212  gfx::Vector2d offset;
213
214  NSEvent* event =
215      TestMouseEvent(kCGEventLeftMouseDown, location, kNoEventFlags);
216  EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event));
217  EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, ui::EventFlagsFromNative(event));
218  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
219
220  event =
221      TestMouseEvent(kCGEventOtherMouseDown, location, kCGEventFlagMaskShift);
222  EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event));
223  EXPECT_EQ(ui::EF_MIDDLE_MOUSE_BUTTON | ui::EF_SHIFT_DOWN,
224            ui::EventFlagsFromNative(event));
225  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
226
227  event = TestMouseEvent(kCGEventRightMouseUp, location, kNoEventFlags);
228  EXPECT_EQ(ui::ET_MOUSE_RELEASED, ui::EventTypeFromNative(event));
229  EXPECT_EQ(ui::EF_RIGHT_MOUSE_BUTTON, ui::EventFlagsFromNative(event));
230  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
231
232  // Scroll up.
233  event = TestScrollEvent(location, 0, 1);
234  EXPECT_EQ(ui::ET_SCROLL, ui::EventTypeFromNative(event));
235  EXPECT_EQ(0, ui::EventFlagsFromNative(event));
236  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
237  offset = ui::GetMouseWheelOffset(event);
238  EXPECT_GT(offset.y(), 0);
239  EXPECT_EQ(0, offset.x());
240
241  // Scroll down.
242  event = TestScrollEvent(location, 0, -1);
243  EXPECT_EQ(ui::ET_SCROLL, ui::EventTypeFromNative(event));
244  EXPECT_EQ(0, ui::EventFlagsFromNative(event));
245  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
246  offset = ui::GetMouseWheelOffset(event);
247  EXPECT_LT(offset.y(), 0);
248  EXPECT_EQ(0, offset.x());
249
250  // Scroll left.
251  event = TestScrollEvent(location, 1, 0);
252  EXPECT_EQ(ui::ET_SCROLL, ui::EventTypeFromNative(event));
253  EXPECT_EQ(0, ui::EventFlagsFromNative(event));
254  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
255  offset = ui::GetMouseWheelOffset(event);
256  EXPECT_EQ(0, offset.y());
257  EXPECT_GT(offset.x(), 0);
258
259  // Scroll right.
260  event = TestScrollEvent(location, -1, 0);
261  EXPECT_EQ(ui::ET_SCROLL, ui::EventTypeFromNative(event));
262  EXPECT_EQ(0, ui::EventFlagsFromNative(event));
263  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
264  offset = ui::GetMouseWheelOffset(event);
265  EXPECT_EQ(0, offset.y());
266  EXPECT_LT(offset.x(), 0);
267}
268
269// Test correct location when the window has a native titlebar.
270TEST_F(EventsMacTest, NativeTitlebarEventLocation) {
271  gfx::Point location(5, 10);
272  NSUInteger style_mask = NSTitledWindowMask | NSClosableWindowMask |
273                          NSMiniaturizableWindowMask | NSResizableWindowMask;
274
275  // First check that the window provided by ui::CocoaTest is how we think.
276  DCHECK_EQ(NSBorderlessWindowMask, [test_window() styleMask]);
277  [test_window() setStyleMask:style_mask];
278  DCHECK_EQ(style_mask, [test_window() styleMask]);
279
280  // EventLocationFromNative should behave the same as the ButtonEvents test.
281  NSEvent* event =
282      TestMouseEvent(kCGEventLeftMouseDown, location, kNoEventFlags);
283  EXPECT_EQ(ui::ET_MOUSE_PRESSED, ui::EventTypeFromNative(event));
284  EXPECT_EQ(ui::EF_LEFT_MOUSE_BUTTON, ui::EventFlagsFromNative(event));
285  EXPECT_EQ(location, gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
286
287  // And be explicit, to ensure the test doesn't depend on some property of the
288  // test harness. The change to the frame rect could be OS-specfic, so set it
289  // to a known value.
290  const CGFloat kTestHeight = 400;
291  NSRect content_rect = NSMakeRect(0, 0, 600, kTestHeight);
292  NSRect frame_rect = [test_window() frameRectForContentRect:content_rect];
293  [test_window() setFrame:frame_rect display:YES];
294  event = [NSEvent mouseEventWithType:NSLeftMouseDown
295                             location:NSMakePoint(0, 0)  // Bottom-left corner.
296                        modifierFlags:0
297                            timestamp:0
298                         windowNumber:[test_window() windowNumber]
299                              context:nil
300                          eventNumber:0
301                           clickCount:0
302                             pressure:1.0];
303  // Bottom-left corner should be flipped.
304  EXPECT_EQ(gfx::Point(0, kTestHeight),
305            gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
306
307  // Removing the border, and sending the same event should move it down in the
308  // toolkit-views coordinate system.
309  int height_change = NSHeight(frame_rect) - kTestHeight;
310  EXPECT_GT(height_change, 0);
311  [test_window() setStyleMask:NSBorderlessWindowMask];
312  [test_window() setFrame:frame_rect display:YES];
313  EXPECT_EQ(gfx::Point(0, kTestHeight + height_change),
314            gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
315}
316
317// Test that window-less events preserve location (are in screen coordinates).
318TEST_F(EventsMacTest, NoWindowLocation) {
319  const CGPoint location = CGPointMake(5, 10);
320
321  base::ScopedCFTypeRef<CGEventRef> mouse(CGEventCreateMouseEvent(
322      nullptr, kCGEventMouseMoved, location, kCGMouseButtonLeft));
323
324  NSEvent* event = [NSEvent eventWithCGEvent:mouse];
325  EXPECT_FALSE(event.window);
326  EXPECT_EQ(gfx::Point(location),
327            gfx::ToFlooredPoint(ui::EventLocationFromNative(event)));
328}
329
330// Testing for ui::EventTypeFromNative() not covered by ButtonEvents.
331TEST_F(EventsMacTest, EventTypeFromNative) {
332  NSEvent* event = cocoa_test_event_utils::KeyEventWithType(NSKeyDown, 0);
333  EXPECT_EQ(ui::ET_KEY_PRESSED, ui::EventTypeFromNative(event));
334
335  event = cocoa_test_event_utils::KeyEventWithType(NSKeyUp, 0);
336  EXPECT_EQ(ui::ET_KEY_RELEASED, ui::EventTypeFromNative(event));
337
338  event = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseDragged, 0);
339  EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event));
340  event = cocoa_test_event_utils::MouseEventWithType(NSRightMouseDragged, 0);
341  EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event));
342  event = cocoa_test_event_utils::MouseEventWithType(NSOtherMouseDragged, 0);
343  EXPECT_EQ(ui::ET_MOUSE_DRAGGED, ui::EventTypeFromNative(event));
344
345  event = cocoa_test_event_utils::MouseEventWithType(NSMouseMoved, 0);
346  EXPECT_EQ(ui::ET_MOUSE_MOVED, ui::EventTypeFromNative(event));
347
348  event = cocoa_test_event_utils::EnterEvent();
349  EXPECT_EQ(ui::ET_MOUSE_ENTERED, ui::EventTypeFromNative(event));
350  event = cocoa_test_event_utils::ExitEvent();
351  EXPECT_EQ(ui::ET_MOUSE_EXITED, ui::EventTypeFromNative(event));
352}
353
354// Verify that a mouse wheel scroll event is correctly lacking phase data.
355TEST_F(EventsMacTest, MouseWheelScroll) {
356  int32_t wheel_delta_y = 2;
357
358  NSEvent* ns_wheel = TestScrollEvent(default_location_, 0, wheel_delta_y);
359  EXPECT_FALSE([ns_wheel hasPreciseScrollingDeltas]);
360  ui::ScrollEvent wheel(ns_wheel);
361  EXPECT_EQ(ui::ET_SCROLL, wheel.type());
362
363  // Currently wheel events still say two for finger count, but this may change.
364  EXPECT_EQ(2, wheel.finger_count());
365
366  // Note the phase is "end" for wheel events, not "none". There is always an
367  // "end" when no more events are expected.
368  EXPECT_EQ(ui::EventMomentumPhase::END, wheel.momentum_phase());
369  EXPECT_EQ(default_location_, wheel.location());
370
371  float pixel_delta_y = wheel_delta_y * ui::kScrollbarPixelsPerCocoaTick;
372  EXPECT_EQ(pixel_delta_y, wheel.y_offset_ordinal());
373  EXPECT_EQ(0, wheel.x_offset_ordinal());
374}
375
376// Test the event flow for a trackpad "rest" that doesn't result in scrolling
377// nor momentum. Also check the boring stuff like type, finger count and
378// location, which isn't phase-specific.
379// Sequence:
380// (1) NSEvent: type=ScrollWheel loc=(780,41) time=14909.3 flags=0x100 win=<set>
381//     {deviceD,d}elta{X,Y,Z}=0 count:0 phase=MayBegin momentumPhase=None
382// (2) NSEvent: type=ScrollWheel loc=(780,41) time=14912.9 flags=0x100 win=<set>
383//     {deviceD,d}elta{X,Y,Z}=0 count:0 phase=Cancelled momentumPhase=None.
384TEST_F(EventsMacTest, TrackpadRestRelease) {
385  NSArray* ns_events = TrackpadScrollSequence(true, 0, 0);
386  ASSERT_EQ(2u, [ns_events count]);
387  EXPECT_TRUE([ns_events[0] hasPreciseScrollingDeltas]);
388
389  ui::ScrollEvent rest(ns_events[0]);
390  EXPECT_EQ(ui::ET_SCROLL, rest.type());
391  EXPECT_EQ(2, rest.finger_count());
392  EXPECT_EQ(ui::EventMomentumPhase::MAY_BEGIN, rest.momentum_phase());
393  EXPECT_EQ(0, rest.y_offset_ordinal());
394  EXPECT_EQ(default_location_, rest.location());
395
396  ui::ScrollEvent cancel(ns_events[1]);
397  EXPECT_EQ(ui::ET_SCROLL, cancel.type());
398  EXPECT_EQ(2, cancel.finger_count());
399  EXPECT_EQ(ui::EventMomentumPhase::END, cancel.momentum_phase());
400  EXPECT_EQ(0, cancel.y_offset_ordinal());
401  EXPECT_EQ(default_location_, cancel.location());
402}
403
404// Test the event flow for touching the trackpad while "in motion" already, then
405// pausing so that a flick is not generated. deltaX and deltaZ are always zero.
406// Note, deviceDeltaX may take on an integer value even though deltaX is zero.
407// Sequence:
408// (1) NSEvent: type=ScrollWheel loc=(780,41) time=15263.2 flags=0x100 win=<set>
409//     deltaY=0.000000 deviceDeltaY=1.000000 phase=Began momentumPhase=None
410// (n) NSEvent: type=ScrollWheel loc=(780,41) time=15263.2 flags=0x100 win=<set>
411//     deltaY=0.400024 deviceDeltaY=3.000000 phase=Changed momentumPhase=None
412// (3) NSEvent: type=ScrollWheel loc=(780,41) time=15264.2 flags=0x100 win=<set>
413//     deltaY=0.000000 deviceDeltaY=0.000000 phase=Ended momentumPhase=None.
414TEST_F(EventsMacTest, TrackpadScrollThenRest) {
415  int32_t delta_y = 21;
416
417  NSArray* ns_events = TrackpadScrollSequence(false, delta_y, 0);
418  ASSERT_EQ(3u, [ns_events count]);
419
420  ui::ScrollEvent begin(ns_events[0]);
421  EXPECT_EQ(ui::EventMomentumPhase::MAY_BEGIN, begin.momentum_phase());
422  EXPECT_EQ(0, begin.y_offset_ordinal());
423
424  ui::ScrollEvent update(ns_events[1]);
425  // There's no momentum yet, so phase is none.
426  EXPECT_EQ(ui::EventMomentumPhase::NONE, update.momentum_phase());
427  // Note: No pixel conversion for "precise" deltas.
428  EXPECT_EQ(delta_y, update.y_offset_ordinal());
429
430  ui::ScrollEvent end(ns_events[2]);
431  EXPECT_EQ(ui::EventMomentumPhase::END, end.momentum_phase());
432  EXPECT_EQ(0, end.y_offset_ordinal());
433}
434
435// Same as the above, but with an initial rest, which is not cancelled. This
436// results in multiple MAY_BEGIN phases.
437TEST_F(EventsMacTest, TrackpadRestThenScrollThenRest) {
438  int32_t delta_y = 21;
439
440  NSArray* ns_events = TrackpadScrollSequence(true, delta_y, 0);
441  ASSERT_EQ(4u, [ns_events count]);
442
443  ui::ScrollEvent rest(ns_events[0]);
444  EXPECT_EQ(ui::EventMomentumPhase::MAY_BEGIN, rest.momentum_phase());
445  EXPECT_EQ(0, rest.y_offset_ordinal());
446
447  ui::ScrollEvent begin(ns_events[1]);
448  EXPECT_EQ(ui::EventMomentumPhase::MAY_BEGIN, begin.momentum_phase());
449  EXPECT_EQ(0, begin.y_offset_ordinal());
450
451  ui::ScrollEvent update(ns_events[2]);
452  EXPECT_EQ(ui::EventMomentumPhase::NONE, update.momentum_phase());
453  EXPECT_EQ(delta_y, update.y_offset_ordinal());
454
455  ui::ScrollEvent end(ns_events[3]);
456  EXPECT_EQ(ui::EventMomentumPhase::END, end.momentum_phase());
457  EXPECT_EQ(0, end.y_offset_ordinal());
458}
459
460// Test the event flows that lead to momentum, with and without an initial rest.
461// Example sequence (no initial rest):
462// (1) NSEvent: type=ScrollWheel loc=(780,41) time=15187.5 flags=0x100 win=<set>
463//     deltaY=0.000000 deviceDeltaY=1.000000 phase=Began momentumPhase=None
464// (n) NSEvent: type=ScrollWheel loc=(780,41) time=15187.5 flags=0x100 win=<set>
465//     deltaY=0.500031  deviceDeltaY=4.000000 phase=Changed momentumPhase=None
466// (3) NSEvent: type=ScrollWheel loc=(780,41) time=15187.6 flags=0x100 win=<set>
467//     deltaY=0.000000 deviceDeltaY=0.000000 phase=Ended momentumPhase=None
468// (4) NSEvent: type=ScrollWheel loc=(780,41) time=15187.6 flags=0x100 win=<set>
469//     deltaY=0.900055 deviceDeltaY=3.000000 phase=None momentumPhase=Began
470// (n) NSEvent: type=ScrollWheel loc=(780,41) time=15187.6 flags=0x100 win=<set>
471//     deltaY=0.300018 deviceDeltaY=3.000000 phase=None momentumPhase=Changed
472// (6) NSEvent: type=ScrollWheel loc=(780,41) time=15188.0 flags=0x100 win=<set>
473//     deltaY=0.000000 deviceDeltaY=0.000000 phase=None momentumPhase=Ended.
474TEST_F(EventsMacTest, TrackpadScrollThenFlick) {
475  int32_t delta_y = 21;
476  int32_t momentum_delta_y = 33;
477
478  NSArray* ns_events = TrackpadScrollSequence(false, delta_y, momentum_delta_y);
479  ASSERT_EQ(6u, [ns_events count]);
480
481  // Non-momentum part.
482  {
483    ui::ScrollEvent begin(ns_events[0]);
484    EXPECT_EQ(ui::EventMomentumPhase::MAY_BEGIN, begin.momentum_phase());
485    EXPECT_EQ(0, begin.y_offset_ordinal());
486
487    ui::ScrollEvent update(ns_events[1]);
488    EXPECT_EQ(ui::EventMomentumPhase::NONE, update.momentum_phase());
489    EXPECT_EQ(delta_y, update.y_offset_ordinal());
490
491    ui::ScrollEvent end(ns_events[2]);
492    // Even though the event stream continues, AppKit doesn't provide a way to
493    // know this without peeking at future events. So this "end" mid-stream is
494    // unavoidable.
495    EXPECT_EQ(ui::EventMomentumPhase::END, end.momentum_phase());
496    EXPECT_EQ(0, end.y_offset_ordinal());
497  }
498  // Momentum part.
499  {
500    ui::ScrollEvent begin(ns_events[3]);
501    // Since a momentum "begin" is really a continuation of the stream, it's
502    // currently treated as an update, but the offsets should always be zero.
503    EXPECT_EQ(ui::EventMomentumPhase::INERTIAL_UPDATE, begin.momentum_phase());
504    EXPECT_EQ(0, begin.y_offset_ordinal());
505
506    ui::ScrollEvent update(ns_events[4]);
507    EXPECT_EQ(ui::EventMomentumPhase::INERTIAL_UPDATE, update.momentum_phase());
508    EXPECT_EQ(momentum_delta_y, update.y_offset_ordinal());
509
510    ui::ScrollEvent end(ns_events[5]);
511    EXPECT_EQ(ui::EventMomentumPhase::END, end.momentum_phase());
512    EXPECT_EQ(0, end.y_offset_ordinal());
513  }
514}
515
516// Check that NSFlagsChanged event is translated to key press or release event.
517TEST_F(EventsMacTest, HandleModifierOnlyKeyEvents) {
518  struct {
519    const char* description;
520    NSEventModifierFlags modifier_flags;
521    uint16_t key_code;
522    EventType expected_type;
523    KeyboardCode expected_key_code;
524  } test_cases[] = {
525      {"CapsLock pressed", NSAlphaShiftKeyMask, kVK_CapsLock, ET_KEY_PRESSED,
526       VKEY_CAPITAL},
527      {"CapsLock released", 0, kVK_CapsLock, ET_KEY_RELEASED, VKEY_CAPITAL},
528      {"Shift pressed", NSShiftKeyMask, kVK_Shift, ET_KEY_PRESSED, VKEY_SHIFT},
529      {"Shift released", 0, kVK_Shift, ET_KEY_RELEASED, VKEY_SHIFT},
530      {"Control pressed", NSControlKeyMask, kVK_Control, ET_KEY_PRESSED,
531       VKEY_CONTROL},
532      {"Control released", 0, kVK_Control, ET_KEY_RELEASED, VKEY_CONTROL},
533      {"Option pressed", NSAlternateKeyMask, kVK_Option, ET_KEY_PRESSED,
534       VKEY_MENU},
535      {"Option released", 0, kVK_Option, ET_KEY_RELEASED, VKEY_MENU},
536      {"Command pressed", NSCommandKeyMask, kVK_Command, ET_KEY_PRESSED,
537       VKEY_LWIN},
538      {"Command released", 0, kVK_Command, ET_KEY_RELEASED, VKEY_LWIN},
539      {"Shift pressed with CapsLock on", NSShiftKeyMask | NSAlphaShiftKeyMask,
540       kVK_Shift, ET_KEY_PRESSED, VKEY_SHIFT},
541      {"Shift released with CapsLock off", NSAlphaShiftKeyMask, kVK_Shift,
542       ET_KEY_RELEASED, VKEY_SHIFT},
543  };
544  for (const auto& test_case : test_cases) {
545    SCOPED_TRACE(::testing::Message() << "While checking case: "
546                                      << test_case.description);
547    NSEvent* native_event = cocoa_test_event_utils::KeyEventWithModifierOnly(
548        test_case.key_code, test_case.modifier_flags);
549    std::unique_ptr<ui::Event> event = EventFromNative(native_event);
550    EXPECT_TRUE(event);
551    EXPECT_EQ(test_case.expected_type, event->type());
552    EXPECT_EQ(test_case.expected_key_code, event->AsKeyEvent()->key_code());
553  }
554}
555
556}  // namespace ui
557