gesture_binding_test.dart 20.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' as ui;

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/gestures.dart';
9
import 'package:flutter/scheduler.dart';
10
import 'package:flutter_test/flutter_test.dart';
11

12
typedef HandleEventCallback = void Function(PointerEvent event);
13

14
class TestGestureFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding {
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
  }

  /// The singleton instance of this object.
  ///
  /// Provides access to the features exposed by this class. The binding must
  /// be initialized before using this getter; this is typically done by calling
  /// [TestGestureFlutterBinding.ensureInitialized].
  static TestGestureFlutterBinding get instance => BindingBase.checkInstance(_instance);
  static TestGestureFlutterBinding? _instance;

  /// Returns an instance of the [TestGestureFlutterBinding], creating and
  /// initializing it if necessary.
  static TestGestureFlutterBinding ensureInitialized() {
    if (_instance == null) {
      TestGestureFlutterBinding();
    }
    return _instance!;
  }

38 39 40 41 42 43 44 45 46
  HandleEventCallback? onHandlePointerEvent;

  @override
  void handlePointerEvent(PointerEvent event) {
    onHandlePointerEvent?.call(event);
    super.handlePointerEvent(event);
  }

  HandleEventCallback? onHandleEvent;
47 48 49

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
50
    super.handleEvent(event, entry);
51
    onHandleEvent?.call(event);
52
  }
53 54 55
}

void main() {
56
  final TestGestureFlutterBinding binding = TestGestureFlutterBinding.ensureInitialized();
57 58

  test('Pointer tap events', () {
59 60 61 62
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.down),
        ui.PointerData(change: ui.PointerChange.up),
63
      ],
64
    );
65

66
    final List<PointerEvent> events = <PointerEvent>[];
67
    TestGestureFlutterBinding.instance.onHandleEvent = events.add;
68

69
    GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet);
70
    expect(events.length, 2);
71 72
    expect(events[0], isA<PointerDownEvent>());
    expect(events[1], isA<PointerUpEvent>());
73 74
  });

75
  test('Pointer move events', () {
76 77 78 79 80
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.down),
        ui.PointerData(change: ui.PointerChange.move),
        ui.PointerData(change: ui.PointerChange.up),
81
      ],
82
    );
83

84
    final List<PointerEvent> events = <PointerEvent>[];
85
    binding.onHandleEvent = events.add;
86

87
    GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet);
88
    expect(events.length, 3);
89 90 91
    expect(events[0], isA<PointerDownEvent>());
    expect(events[1], isA<PointerMoveEvent>());
    expect(events[2], isA<PointerUpEvent>());
92 93
  });

94 95 96
  test('Pointer hover events', () {
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
        data: <ui.PointerData>[
97
          ui.PointerData(change: ui.PointerChange.add),
98 99 100
          ui.PointerData(change: ui.PointerChange.hover),
          ui.PointerData(change: ui.PointerChange.hover),
          ui.PointerData(change: ui.PointerChange.remove),
101
          ui.PointerData(change: ui.PointerChange.add),
102
          ui.PointerData(change: ui.PointerChange.hover),
103
        ],
104 105 106
    );

    final List<PointerEvent> pointerRouterEvents = <PointerEvent>[];
107
    GestureBinding.instance.pointerRouter.addGlobalRoute(pointerRouterEvents.add);
108 109

    final List<PointerEvent> events = <PointerEvent>[];
110
    binding.onHandleEvent = events.add;
111

112
    GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet);
113 114 115 116
    expect(events.length, 3);
    expect(events[0], isA<PointerHoverEvent>());
    expect(events[1], isA<PointerHoverEvent>());
    expect(events[2], isA<PointerHoverEvent>());
117
    expect(pointerRouterEvents.length, 6, reason: 'pointerRouterEvents contains: $pointerRouterEvents');
118 119 120 121 122 123
    expect(pointerRouterEvents[0], isA<PointerAddedEvent>());
    expect(pointerRouterEvents[1], isA<PointerHoverEvent>());
    expect(pointerRouterEvents[2], isA<PointerHoverEvent>());
    expect(pointerRouterEvents[3], isA<PointerRemovedEvent>());
    expect(pointerRouterEvents[4], isA<PointerAddedEvent>());
    expect(pointerRouterEvents[5], isA<PointerHoverEvent>());
124 125
  });

126
  test('Pointer cancel events', () {
127 128 129
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.down),
130
        ui.PointerData(),
131
      ],
132
    );
133

134
    final List<PointerEvent> events = <PointerEvent>[];
135
    binding.onHandleEvent = events.add;
136

137
    GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet);
138
    expect(events.length, 2);
139 140
    expect(events[0], isA<PointerDownEvent>());
    expect(events[1], isA<PointerCancelEvent>());
141 142 143
  });

  test('Can cancel pointers', () {
144 145 146 147
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.down),
        ui.PointerData(change: ui.PointerChange.up),
148
      ],
149
    );
150

151
    final List<PointerEvent> events = <PointerEvent>[];
152
    binding.onHandleEvent = (PointerEvent event) {
153
      events.add(event);
154
      if (event is PointerDownEvent) {
155
        binding.cancelPointer(event.pointer);
156
      }
157 158
    };

159
    GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet);
160
    expect(events.length, 2);
161 162
    expect(events[0], isA<PointerDownEvent>());
    expect(events[1], isA<PointerCancelEvent>());
163
  });
164

165 166
  const double devicePixelRatio = 2.5;

167
  test('Can expand add and hover pointers', () {
168 169 170 171 172
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.add, device: 24),
        ui.PointerData(change: ui.PointerChange.hover, device: 24),
        ui.PointerData(change: ui.PointerChange.remove, device: 24),
173
        ui.PointerData(change: ui.PointerChange.add, device: 24),
174
        ui.PointerData(change: ui.PointerChange.hover, device: 24),
175
      ],
176 177
    );

178
    final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
179 180

    expect(events.length, 5);
181 182 183 184 185
    expect(events[0], isA<PointerAddedEvent>());
    expect(events[1], isA<PointerHoverEvent>());
    expect(events[2], isA<PointerRemovedEvent>());
    expect(events[3], isA<PointerAddedEvent>());
    expect(events[4], isA<PointerHoverEvent>());
186 187
  });

188 189 190 191 192 193
  test('Can handle malformed scrolling event.', () {
    ui.PointerDataPacket packet = const ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.add, device: 24),
      ],
    );
194
    List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209

    expect(events.length, 1);
    expect(events[0], isA<PointerAddedEvent>());

    // Send packet contains malformed scroll events.
    packet = const ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaX: double.infinity, scrollDeltaY: 10),
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaX: double.nan, scrollDeltaY: 10),
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaX: double.negativeInfinity, scrollDeltaY: 10),
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaY: double.infinity, scrollDeltaX: 10),
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaY: double.nan, scrollDeltaX: 10),
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaY: double.negativeInfinity, scrollDeltaX: 10),
      ],
    );
210
    events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
211 212 213 214 215 216 217 218 219
    expect(events.length, 0);

    // Send packet with a valid scroll event.
    packet = const ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(signalKind: ui.PointerSignalKind.scroll, device: 24, scrollDeltaX: 10, scrollDeltaY: 10),
      ],
    );
    // Make sure PointerEventConverter can expand when device pixel ratio is valid.
220
    events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
221 222 223 224
    expect(events.length, 1);
    expect(events[0], isA<PointerScrollEvent>());

    // Make sure PointerEventConverter returns none when device pixel ratio is invalid.
225
    events = PointerEventConverter.expand(packet.data, (int viewId) => 0).toList();
226 227 228
    expect(events.length, 0);
  });

229 230 231 232 233
  test('Can expand pointer scroll events', () {
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
        data: <ui.PointerData>[
          ui.PointerData(change: ui.PointerChange.add),
          ui.PointerData(change: ui.PointerChange.hover, signalKind: ui.PointerSignalKind.scroll),
234
        ],
235 236
    );

237
    final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
238 239

    expect(events.length, 2);
240 241
    expect(events[0], isA<PointerAddedEvent>());
    expect(events[1], isA<PointerScrollEvent>());
242 243
  });

244
  test('Should synthesize kPrimaryButton for touch when no button is set', () {
245
    final Offset location = const Offset(10.0, 10.0) * devicePixelRatio;
246 247
    final ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
248 249 250 251 252
        ui.PointerData(change: ui.PointerChange.add, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.hover, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.down, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.move, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.up, physicalX: location.dx, physicalY: location.dy),
253
      ],
254 255
    );

256
    final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
257 258

    expect(events.length, 5);
259
    expect(events[0], isA<PointerAddedEvent>());
260
    expect(events[0].buttons, equals(0));
261
    expect(events[1], isA<PointerHoverEvent>());
262
    expect(events[1].buttons, equals(0));
263
    expect(events[2], isA<PointerDownEvent>());
264
    expect(events[2].buttons, equals(kPrimaryButton));
265
    expect(events[3], isA<PointerMoveEvent>());
266
    expect(events[3].buttons, equals(kPrimaryButton));
267
    expect(events[4], isA<PointerUpEvent>());
268 269 270
    expect(events[4].buttons, equals(0));
  });

271
  test('Should not synthesize kPrimaryButton for touch when a button is set', () {
272
    final Offset location = const Offset(10.0, 10.0) * devicePixelRatio;
273 274
    final ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
275 276 277 278 279
        ui.PointerData(change: ui.PointerChange.add, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.hover, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.down, buttons: kSecondaryButton, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.move, buttons: kSecondaryButton, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.up, physicalX: location.dx, physicalY: location.dy),
280 281 282
      ],
    );

283
    final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298

    expect(events.length, 5);
    expect(events[0], isA<PointerAddedEvent>());
    expect(events[0].buttons, equals(0));
    expect(events[1], isA<PointerHoverEvent>());
    expect(events[1].buttons, equals(0));
    expect(events[2], isA<PointerDownEvent>());
    expect(events[2].buttons, equals(kSecondaryButton));
    expect(events[3], isA<PointerMoveEvent>());
    expect(events[3].buttons, equals(kSecondaryButton));
    expect(events[4], isA<PointerUpEvent>());
    expect(events[4].buttons, equals(0));
  });

  test('Should synthesize kPrimaryButton for stylus when no button is set', () {
299
    final Offset location = const Offset(10.0, 10.0) * devicePixelRatio;
300
    for (final PointerDeviceKind kind in <PointerDeviceKind>[
301 302 303 304 305 306 307 308 309 310 311
      PointerDeviceKind.stylus,
      PointerDeviceKind.invertedStylus,
    ]) {

      final ui.PointerDataPacket packet = ui.PointerDataPacket(
        data: <ui.PointerData>[
          ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.down, kind: kind, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.move, buttons: kSecondaryStylusButton, kind: kind, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
312
        ],
313 314
      );

315
      final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
316 317

      expect(events.length, 5);
318
      expect(events[0], isA<PointerAddedEvent>());
319
      expect(events[0].buttons, equals(0));
320
      expect(events[1], isA<PointerHoverEvent>());
321
      expect(events[1].buttons, equals(0));
322
      expect(events[2], isA<PointerDownEvent>());
323
      expect(events[2].buttons, equals(kPrimaryButton));
324
      expect(events[3], isA<PointerMoveEvent>());
325
      expect(events[3].buttons, equals(kSecondaryStylusButton));
326
      expect(events[4], isA<PointerUpEvent>());
327 328 329 330
      expect(events[4].buttons, equals(0));
    }
  });

331
  test('Should synthesize kPrimaryButton for unknown devices when no button is set', () {
332
    final Offset location = const Offset(10.0, 10.0) * devicePixelRatio;
333 334 335 336 337 338
    const PointerDeviceKind kind = PointerDeviceKind.unknown;
    final ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
        ui.PointerData(change: ui.PointerChange.down, kind: kind, physicalX: location.dx, physicalY: location.dy),
339
        ui.PointerData(change: ui.PointerChange.move, buttons: kSecondaryButton, kind: kind, physicalX: location.dx, physicalY: location.dy),
340
        ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
341
      ],
342 343
    );

344
    final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
345 346

    expect(events.length, 5);
347
    expect(events[0], isA<PointerAddedEvent>());
348
    expect(events[0].buttons, equals(0));
349
    expect(events[1], isA<PointerHoverEvent>());
350
    expect(events[1].buttons, equals(0));
351
    expect(events[2], isA<PointerDownEvent>());
352
    expect(events[2].buttons, equals(kPrimaryButton));
353
    expect(events[3], isA<PointerMoveEvent>());
354
    expect(events[3].buttons, equals(kSecondaryButton));
355
    expect(events[4], isA<PointerUpEvent>());
356 357 358
    expect(events[4].buttons, equals(0));
  });

359
  test('Should not synthesize kPrimaryButton for mouse', () {
360
    final Offset location = const Offset(10.0, 10.0) * devicePixelRatio;
361
    for (final PointerDeviceKind kind in <PointerDeviceKind>[
362 363 364 365 366 367 368 369 370
      PointerDeviceKind.mouse,
    ]) {
      final ui.PointerDataPacket packet = ui.PointerDataPacket(
        data: <ui.PointerData>[
          ui.PointerData(change: ui.PointerChange.add, kind: kind, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.hover, kind: kind, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.down, kind: kind, buttons: kMiddleMouseButton, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.move, kind: kind, buttons: kMiddleMouseButton | kSecondaryMouseButton, physicalX: location.dx, physicalY: location.dy),
          ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy),
371
        ],
372 373
      );

374
      final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList();
375 376

      expect(events.length, 5);
377
      expect(events[0], isA<PointerAddedEvent>());
378
      expect(events[0].buttons, equals(0));
379
      expect(events[1], isA<PointerHoverEvent>());
380
      expect(events[1].buttons, equals(0));
381
      expect(events[2], isA<PointerDownEvent>());
382
      expect(events[2].buttons, equals(kMiddleMouseButton));
383
      expect(events[3], isA<PointerMoveEvent>());
384
      expect(events[3].buttons, equals(kMiddleMouseButton | kSecondaryMouseButton));
385
      expect(events[4], isA<PointerUpEvent>());
386 387 388
      expect(events[4].buttons, equals(0));
    }
  });
389 390 391 392 393 394 395 396 397 398 399

  test('Pointer pan/zoom events', () {
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.panZoomStart),
        ui.PointerData(change: ui.PointerChange.panZoomUpdate),
        ui.PointerData(change: ui.PointerChange.panZoomEnd),
      ],
    );

    final List<PointerEvent> events = <PointerEvent>[];
400
    binding.onHandleEvent = events.add;
401

402
    binding.platformDispatcher.onPointerDataPacket?.call(packet);
403 404 405 406 407
    expect(events.length, 3);
    expect(events[0], isA<PointerPanZoomStartEvent>());
    expect(events[1], isA<PointerPanZoomUpdateEvent>());
    expect(events[2], isA<PointerPanZoomEndEvent>());
  });
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431

  test('Error handling', () {
    const ui.PointerDataPacket packet = ui.PointerDataPacket(
      data: <ui.PointerData>[
        ui.PointerData(change: ui.PointerChange.down),
        ui.PointerData(change: ui.PointerChange.up),
      ],
    );

    final List<String> events = <String>[];
    binding.onHandlePointerEvent = (PointerEvent event) { throw Exception('zipzapzooey $event'); };
    FlutterError.onError = (FlutterErrorDetails details) { events.add(details.toString()); };
    try {
      GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet);
      expect(events.length, 1);
      expect(events[0], contains('while handling a pointer data\npacket')); // The default stringifying behavior uses 65 character wrapWidth.
      expect(events[0], contains('zipzapzooey'));
      expect(events[0], contains('PointerDownEvent'));
      expect(events[0], isNot(contains('PointerUpEvent'))); // Failure happens on the first message, remaining messages aren't processed.
    } finally {
      binding.onHandlePointerEvent = null;
      FlutterError.onError = FlutterError.presentError;
    }
  });
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460

  test('PointerEventConverter processes view IDs', () {
    const int startID = 987654;
    const List<ui.PointerData> data = <ui.PointerData>[
      ui.PointerData(viewId: startID + 0, change: ui.PointerChange.cancel), // ignore: avoid_redundant_argument_values
      ui.PointerData(viewId: startID + 1, change: ui.PointerChange.add),
      ui.PointerData(viewId: startID + 2, change: ui.PointerChange.remove),
      ui.PointerData(viewId: startID + 3, change: ui.PointerChange.hover),
      ui.PointerData(viewId: startID + 4, change: ui.PointerChange.down),
      ui.PointerData(viewId: startID + 5, change: ui.PointerChange.move),
      ui.PointerData(viewId: startID + 6, change: ui.PointerChange.up),
      ui.PointerData(viewId: startID + 7, change: ui.PointerChange.panZoomStart),
      ui.PointerData(viewId: startID + 8, change: ui.PointerChange.panZoomUpdate),
      ui.PointerData(viewId: startID + 9, change: ui.PointerChange.panZoomEnd),
    ];

    final List<int> viewIds = <int>[];
    double devicePixelRatioGetter(int viewId) {
      viewIds.add(viewId);
      return viewId / 10.0;
    }

    final List<PointerEvent> events = PointerEventConverter.expand(data, devicePixelRatioGetter).toList();

    final List<int> expectedViewIds = List<int>.generate(10, (int index) => startID + index);
    expect(viewIds, expectedViewIds);
    expect(events, hasLength(10));
    expect(events.map((PointerEvent event) => event.viewId), expectedViewIds);
  });
461
}