flutter_flutter/packages/flutter/test/gestures/gesture_binding_test.dart
Michael Goderbauer 5491c8c146
Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
2024-12-19 20:06:21 +00:00

628 lines
22 KiB
Dart

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