// Copyright 2014 The Flutter Authors. All rights reserved. // 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; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; typedef HandleEventCallback = void Function(PointerEvent event); class TestGestureFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding { @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!; } HandleEventCallback? onHandlePointerEvent; @override void handlePointerEvent(PointerEvent event) { onHandlePointerEvent?.call(event); super.handlePointerEvent(event); } HandleEventCallback? onHandleEvent; @override void handleEvent(PointerEvent event, HitTestEntry entry) { super.handleEvent(event, entry); onHandleEvent?.call(event); } } void main() { final TestGestureFlutterBinding binding = TestGestureFlutterBinding.ensureInitialized(); test('Pointer tap events', () { const ui.PointerDataPacket packet = ui.PointerDataPacket( data: <ui.PointerData>[ ui.PointerData(change: ui.PointerChange.down), ui.PointerData(change: ui.PointerChange.up), ], ); final List<PointerEvent> events = <PointerEvent>[]; TestGestureFlutterBinding.instance.onHandleEvent = events.add; GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet); expect(events.length, 2); expect(events[0], isA<PointerDownEvent>()); expect(events[1], isA<PointerUpEvent>()); }); test('Pointer move events', () { 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), ], ); final List<PointerEvent> events = <PointerEvent>[]; binding.onHandleEvent = events.add; GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet); expect(events.length, 3); expect(events[0], isA<PointerDownEvent>()); expect(events[1], isA<PointerMoveEvent>()); expect(events[2], isA<PointerUpEvent>()); }); test('Pointer hover events', () { const ui.PointerDataPacket packet = ui.PointerDataPacket( data: <ui.PointerData>[ ui.PointerData(change: ui.PointerChange.add), ui.PointerData(change: ui.PointerChange.hover), ui.PointerData(change: ui.PointerChange.hover), ui.PointerData(change: ui.PointerChange.remove), ui.PointerData(change: ui.PointerChange.add), ui.PointerData(change: ui.PointerChange.hover), ], ); final List<PointerEvent> pointerRouterEvents = <PointerEvent>[]; GestureBinding.instance.pointerRouter.addGlobalRoute(pointerRouterEvents.add); final List<PointerEvent> events = <PointerEvent>[]; binding.onHandleEvent = events.add; GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet); expect(events.length, 3); expect(events[0], isA<PointerHoverEvent>()); expect(events[1], isA<PointerHoverEvent>()); expect(events[2], isA<PointerHoverEvent>()); expect(pointerRouterEvents.length, 6, reason: 'pointerRouterEvents contains: $pointerRouterEvents'); 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>()); }); test('Pointer cancel events', () { const ui.PointerDataPacket packet = ui.PointerDataPacket( data: <ui.PointerData>[ ui.PointerData(change: ui.PointerChange.down), ui.PointerData(), ], ); final List<PointerEvent> events = <PointerEvent>[]; binding.onHandleEvent = events.add; GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet); expect(events.length, 2); expect(events[0], isA<PointerDownEvent>()); expect(events[1], isA<PointerCancelEvent>()); }); test('Can cancel pointers', () { const ui.PointerDataPacket packet = ui.PointerDataPacket( data: <ui.PointerData>[ ui.PointerData(change: ui.PointerChange.down), ui.PointerData(change: ui.PointerChange.up), ], ); final List<PointerEvent> events = <PointerEvent>[]; binding.onHandleEvent = (PointerEvent event) { events.add(event); if (event is PointerDownEvent) { binding.cancelPointer(event.pointer); } }; GestureBinding.instance.platformDispatcher.onPointerDataPacket?.call(packet); expect(events.length, 2); expect(events[0], isA<PointerDownEvent>()); expect(events[1], isA<PointerCancelEvent>()); }); const double devicePixelRatio = 2.5; test('Can expand add and hover pointers', () { 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), ui.PointerData(change: ui.PointerChange.add, device: 24), ui.PointerData(change: ui.PointerChange.hover, device: 24), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 5); 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>()); }); test('Can handle malformed scrolling event.', () { ui.PointerDataPacket packet = const ui.PointerDataPacket( data: <ui.PointerData>[ ui.PointerData(change: ui.PointerChange.add, device: 24), ], ); List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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), ], ); events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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. events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 1); expect(events[0], isA<PointerScrollEvent>()); // Make sure PointerEventConverter returns none when device pixel ratio is invalid. events = PointerEventConverter.expand(packet.data, (int viewId) => 0).toList(); expect(events.length, 0); }); 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), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); expect(events.length, 2); expect(events[0], isA<PointerAddedEvent>()); expect(events[1], isA<PointerScrollEvent>()); }); test('Should synthesize kPrimaryButton for touch when no button is set', () { final Offset location = const Offset(10.0, 10.0) * devicePixelRatio; final ui.PointerDataPacket packet = ui.PointerDataPacket( data: <ui.PointerData>[ 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), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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(kPrimaryButton)); expect(events[3], isA<PointerMoveEvent>()); expect(events[3].buttons, equals(kPrimaryButton)); expect(events[4], isA<PointerUpEvent>()); expect(events[4].buttons, equals(0)); }); test('Should not synthesize kPrimaryButton for touch when a button is set', () { final Offset location = const Offset(10.0, 10.0) * devicePixelRatio; final ui.PointerDataPacket packet = ui.PointerDataPacket( data: <ui.PointerData>[ 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), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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', () { final Offset location = const Offset(10.0, 10.0) * devicePixelRatio; for (final PointerDeviceKind kind in <PointerDeviceKind>[ 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), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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(kPrimaryButton)); expect(events[3], isA<PointerMoveEvent>()); expect(events[3].buttons, equals(kSecondaryStylusButton)); expect(events[4], isA<PointerUpEvent>()); expect(events[4].buttons, equals(0)); } }); test('Should synthesize kPrimaryButton for unknown devices when no button is set', () { final Offset location = const Offset(10.0, 10.0) * devicePixelRatio; 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), ui.PointerData(change: ui.PointerChange.move, buttons: kSecondaryButton, kind: kind, physicalX: location.dx, physicalY: location.dy), ui.PointerData(change: ui.PointerChange.up, kind: kind, physicalX: location.dx, physicalY: location.dy), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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(kPrimaryButton)); expect(events[3], isA<PointerMoveEvent>()); expect(events[3].buttons, equals(kSecondaryButton)); expect(events[4], isA<PointerUpEvent>()); expect(events[4].buttons, equals(0)); }); test('Should not synthesize kPrimaryButton for mouse', () { final Offset location = const Offset(10.0, 10.0) * devicePixelRatio; for (final PointerDeviceKind kind in <PointerDeviceKind>[ 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), ], ); final List<PointerEvent> events = PointerEventConverter.expand(packet.data, (int viewId) => devicePixelRatio).toList(); 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(kMiddleMouseButton)); expect(events[3], isA<PointerMoveEvent>()); expect(events[3].buttons, equals(kMiddleMouseButton | kSecondaryMouseButton)); expect(events[4], isA<PointerUpEvent>()); expect(events[4].buttons, equals(0)); } }); 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>[]; binding.onHandleEvent = events.add; binding.platformDispatcher.onPointerDataPacket?.call(packet); expect(events.length, 3); expect(events[0], isA<PointerPanZoomStartEvent>()); expect(events[1], isA<PointerPanZoomUpdateEvent>()); expect(events[2], isA<PointerPanZoomEndEvent>()); }); 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; } }); 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); }); }