blob: f421762eb9ef7c172fcf4ecd2f71f185d72fa12b [file] [log] [blame]
// 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 'dart:ui' show PointerChange;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'mouse_tracker_test_utils.dart';
MouseTracker get _mouseTracker => RendererBinding.instance.mouseTracker;
typedef SimpleAnnotationFinder = Iterable<TestAnnotationEntry> Function(Offset offset);
void main() {
final TestMouseTrackerFlutterBinding binding = TestMouseTrackerFlutterBinding();
void setUpMouseAnnotationFinder(SimpleAnnotationFinder annotationFinder) {
binding.setHitTest((BoxHitTestResult result, Offset position) {
for (final TestAnnotationEntry entry in annotationFinder(position)) {
result.addWithRawTransform(
transform: entry.transform,
position: position,
hitTest: (BoxHitTestResult result, Offset position) {
result.add(entry);
return true;
},
);
}
return true;
});
}
// Set up a trivial test environment that includes one annotation.
// This annotation records the enter, hover, and exit events it receives to
// `logEvents`.
// This annotation also contains a cursor with a value of `testCursor`.
// The mouse tracker records the cursor requests it receives to `logCursors`.
TestAnnotationTarget setUpWithOneAnnotation({
required List<PointerEvent> logEvents,
}) {
final TestAnnotationTarget oneAnnotation = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) {
if (logEvents != null) {
logEvents.add(event);
}
},
onHover: (PointerHoverEvent event) {
if (logEvents != null) {
logEvents.add(event);
}
},
onExit: (PointerExitEvent event) {
if (logEvents != null) {
logEvents.add(event);
}
},
);
setUpMouseAnnotationFinder(
(Offset position) sync* {
yield TestAnnotationEntry(oneAnnotation);
},
);
return oneAnnotation;
}
void dispatchRemoveDevice([int device = 0]) {
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero, device: device),
]));
}
setUp(() {
binding.postFrameCallbacks.clear();
});
final Matrix4 translate10by20 = Matrix4.translationValues(10, 20, 0);
test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
final List<PointerEvent> events = <PointerEvent>[];
setUpWithOneAnnotation(logEvents: events);
final List<bool> listenerLogs = <bool>[];
_mouseTracker.addListener(() {
listenerLogs.add(_mouseTracker.mouseIsConnected);
});
expect(_mouseTracker.mouseIsConnected, isFalse);
// Pointer enters the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
]));
addTearDown(() => dispatchRemoveDevice());
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent()),
]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();
// Pointer hovers the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 101.0))),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
expect(listenerLogs, isEmpty);
events.clear();
// Pointer is removed while on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(1.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(1.0, 101.0))),
]));
expect(listenerLogs, <bool>[false]);
events.clear();
listenerLogs.clear();
// Pointer is added on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 301.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 301.0))),
]));
expect(listenerLogs, <bool>[true]);
events.clear();
listenerLogs.clear();
});
// Regression test for https://github.com/flutter/flutter/issues/90838
test('should not crash if the first event is a Removed event', () {
final List<PointerEvent> events = <PointerEvent>[];
setUpWithOneAnnotation(logEvents: events);
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, Offset.zero),
]));
events.clear();
});
test('should correctly handle multiple devices', () {
final List<PointerEvent> events = <PointerEvent>[];
setUpWithOneAnnotation(logEvents: events);
expect(_mouseTracker.mouseIsConnected, isFalse);
// The first mouse is added on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, Offset.zero),
_pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent()),
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(0.0, 1.0))),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The second mouse is added on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 401.0), device: 1),
_pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 401.0), device: 1)),
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 401.0), device: 1)),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The first mouse moves on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(0.0, 101.0))),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The second mouse moves on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 501.0), device: 1),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 501.0), device: 1)),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The first mouse is removed while on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 101.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 101.0))),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The second mouse still moves on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(1.0, 601.0), device: 1),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 601.0), device: 1)),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The second mouse is removed while on the annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(1.0, 601.0), device: 1),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(1.0, 601.0), device: 1)),
]));
expect(_mouseTracker.mouseIsConnected, isFalse);
events.clear();
});
test('should not handle non-hover events', () {
final List<PointerEvent> events = <PointerEvent>[];
setUpWithOneAnnotation(logEvents: events);
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.down, const Offset(0.0, 101.0)),
]));
addTearDown(() => dispatchRemoveDevice());
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
// This Enter event is triggered by the [PointerAddedEvent] The
// [PointerDownEvent] is ignored by [MouseTracker].
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 101.0))),
]));
events.clear();
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.move, const Offset(0.0, 201.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
events.clear();
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.up, const Offset(0.0, 301.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
events.clear();
});
test('should correctly handle when the annotation appears or disappears on the pointer', () {
late bool isInHitRegion;
final List<Object> events = <PointerEvent>[];
final TestAnnotationTarget annotation = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegion) {
yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
}
});
isInHitRegion = false;
// Connect a mouse when there is no annotation.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0)),
]));
addTearDown(() => dispatchRemoveDevice());
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// Adding an annotation should trigger Enter event.
isInHitRegion = true;
binding.scheduleMouseTrackerPostFrameCheck();
expect(binding.postFrameCallbacks, hasLength(1));
binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0, 100)).transformed(translate10by20)),
]));
events.clear();
// Removing an annotation should trigger events.
isInHitRegion = false;
binding.scheduleMouseTrackerPostFrameCheck();
expect(binding.postFrameCallbacks, hasLength(1));
binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
]));
expect(binding.postFrameCallbacks, hasLength(0));
});
test('should correctly handle when the annotation moves in or out of the pointer', () {
late bool isInHitRegion;
final List<Object> events = <PointerEvent>[];
final TestAnnotationTarget annotation = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegion) {
yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
}
});
isInHitRegion = false;
// Connect a mouse.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0)),
]));
addTearDown(() => dispatchRemoveDevice());
events.clear();
// During a frame, the annotation moves into the pointer.
isInHitRegion = true;
expect(binding.postFrameCallbacks, hasLength(0));
binding.scheduleMouseTrackerPostFrameCheck();
expect(binding.postFrameCallbacks, hasLength(1));
binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
]));
events.clear();
expect(binding.postFrameCallbacks, hasLength(0));
// During a frame, the annotation moves out of the pointer.
isInHitRegion = false;
expect(binding.postFrameCallbacks, hasLength(0));
binding.scheduleMouseTrackerPostFrameCheck();
expect(binding.postFrameCallbacks, hasLength(1));
binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
]));
expect(binding.postFrameCallbacks, hasLength(0));
});
test('should correctly handle when the pointer is added or removed on the annotation', () {
late bool isInHitRegion;
final List<Object> events = <PointerEvent>[];
final TestAnnotationTarget annotation = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegion) {
yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
}
});
isInHitRegion = false;
// Connect a mouse in the region. Should trigger Enter.
isInHitRegion = true;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0)),
]));
expect(binding.postFrameCallbacks, hasLength(0));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
]));
events.clear();
// Disconnect the mouse from the region. Should trigger Exit.
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 100.0)),
]));
expect(binding.postFrameCallbacks, hasLength(0));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
]));
});
test('should correctly handle when the pointer moves in or out of the annotation', () {
late bool isInHitRegion;
final List<Object> events = <PointerEvent>[];
final TestAnnotationTarget annotation = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegion) {
yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
}
});
isInHitRegion = false;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(200.0, 100.0)),
]));
addTearDown(() => dispatchRemoveDevice());
expect(binding.postFrameCallbacks, hasLength(0));
events.clear();
// Moves the mouse into the region. Should trigger Enter.
isInHitRegion = true;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 100.0)),
]));
expect(binding.postFrameCallbacks, hasLength(0));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
]));
events.clear();
// Moves the mouse out of the region. Should trigger Exit.
isInHitRegion = false;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(200.0, 100.0)),
]));
expect(binding.postFrameCallbacks, hasLength(0));
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(200.0, 100.0)).transformed(translate10by20)),
]));
});
test('should not schedule post-frame callbacks when no mouse is connected', () {
setUpMouseAnnotationFinder((Offset position) sync* {
});
// Connect a touch device, which should not be recognized by MouseTracker
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch),
]));
expect(_mouseTracker.mouseIsConnected, isFalse);
expect(binding.postFrameCallbacks, hasLength(0));
});
test('should not flip out if not all mouse events are listened to', () {
bool isInHitRegionOne = true;
bool isInHitRegionTwo = false;
final TestAnnotationTarget annotation1 = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) {},
);
final TestAnnotationTarget annotation2 = TestAnnotationTarget(
onExit: (PointerExitEvent event) {},
);
setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegionOne) {
yield TestAnnotationEntry(annotation1);
} else if (isInHitRegionTwo) {
yield TestAnnotationEntry(annotation2);
}
});
isInHitRegionOne = false;
isInHitRegionTwo = true;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]));
addTearDown(() => dispatchRemoveDevice());
// Passes if no errors are thrown.
});
test('should trigger callbacks between parents and children in correct order', () {
// This test simulates the scenario of a layer being the child of another.
//
// ———————————
// |A |
// | —————— |
// | |B | |
// | —————— |
// ———————————
late bool isInB;
final List<String> logs = <String>[];
final TestAnnotationTarget annotationA = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => logs.add('enterA'),
onExit: (PointerExitEvent event) => logs.add('exitA'),
onHover: (PointerHoverEvent event) => logs.add('hoverA'),
);
final TestAnnotationTarget annotationB = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => logs.add('enterB'),
onExit: (PointerExitEvent event) => logs.add('exitB'),
onHover: (PointerHoverEvent event) => logs.add('hoverB'),
);
setUpMouseAnnotationFinder((Offset position) sync* {
// Children's annotations come before parents'.
if (isInB) {
yield TestAnnotationEntry(annotationB);
yield TestAnnotationEntry(annotationA);
}
});
// Starts out of A.
isInB = false;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 1.0)),
]));
addTearDown(() => dispatchRemoveDevice());
expect(logs, <String>[]);
// Moves into B within one frame.
isInB = true;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
]));
expect(logs, <String>['enterA', 'enterB', 'hoverB', 'hoverA']);
logs.clear();
// Moves out of A within one frame.
isInB = false;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 20.0)),
]));
expect(logs, <String>['exitB', 'exitA']);
});
test('should trigger callbacks between disjoint siblings in correctly order', () {
// This test simulates the scenario of 2 sibling layers that do not overlap
// with each other.
//
// ———————— ————————
// |A | |B |
// | | | |
// ———————— ————————
late bool isInA;
late bool isInB;
final List<String> logs = <String>[];
final TestAnnotationTarget annotationA = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => logs.add('enterA'),
onExit: (PointerExitEvent event) => logs.add('exitA'),
onHover: (PointerHoverEvent event) => logs.add('hoverA'),
);
final TestAnnotationTarget annotationB = TestAnnotationTarget(
onEnter: (PointerEnterEvent event) => logs.add('enterB'),
onExit: (PointerExitEvent event) => logs.add('exitB'),
onHover: (PointerHoverEvent event) => logs.add('hoverB'),
);
setUpMouseAnnotationFinder((Offset position) sync* {
if (isInA) {
yield TestAnnotationEntry(annotationA);
} else if (isInB) {
yield TestAnnotationEntry(annotationB);
}
});
// Starts within A.
isInA = true;
isInB = false;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 1.0)),
]));
addTearDown(() => dispatchRemoveDevice());
expect(logs, <String>['enterA']);
logs.clear();
// Moves into B within one frame.
isInA = false;
isInB = true;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
]));
expect(logs, <String>['exitA', 'enterB', 'hoverB']);
logs.clear();
// Moves into A within one frame.
isInA = true;
isInB = false;
RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
]));
expect(logs, <String>['exitB', 'enterA', 'hoverA']);
});
}
ui.PointerData _pointerData(
PointerChange change,
Offset logicalPosition, {
int device = 0,
PointerDeviceKind kind = PointerDeviceKind.mouse,
}) {
return ui.PointerData(
change: change,
physicalX: logicalPosition.dx * RendererBinding.instance.window.devicePixelRatio,
physicalY: logicalPosition.dy * RendererBinding.instance.window.devicePixelRatio,
kind: kind,
device: device,
);
}
class BaseEventMatcher extends Matcher {
BaseEventMatcher(this.expected)
: assert(expected != null);
final PointerEvent expected;
bool _matchesField(Map<dynamic, dynamic> matchState, String field, dynamic actual, dynamic expected) {
if (actual != expected) {
addStateInfo(matchState, <dynamic, dynamic>{
'field': field,
'expected': expected,
'actual': actual,
});
return false;
}
return true;
}
@override
bool matches(dynamic untypedItem, Map<dynamic, dynamic> matchState) {
final PointerEvent actual = untypedItem as PointerEvent;
if (!(
_matchesField(matchState, 'kind', actual.kind, PointerDeviceKind.mouse) &&
_matchesField(matchState, 'position', actual.position, expected.position) &&
_matchesField(matchState, 'device', actual.device, expected.device) &&
_matchesField(matchState, 'localPosition', actual.localPosition, expected.localPosition)
)) {
return false;
}
return true;
}
@override
Description describe(Description description) {
return description
.add('event (critical fields only) ')
.addDescriptionOf(expected);
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return mismatchDescription
.add('has ')
.addDescriptionOf(matchState['actual'])
.add(" at field `${matchState['field']}`, which doesn't match the expected ")
.addDescriptionOf(matchState['expected']);
}
}
class EventMatcher<T extends PointerEvent> extends BaseEventMatcher {
EventMatcher(T super.expected);
@override
bool matches(dynamic untypedItem, Map<dynamic, dynamic> matchState) {
if (untypedItem is! T) {
return false;
}
return super.matches(untypedItem, matchState);
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
if (item is! T) {
return mismatchDescription
.add('is ')
.addDescriptionOf(item.runtimeType)
.add(' and is not a subtype of ')
.addDescriptionOf(T);
}
return super.describeMismatch(item, mismatchDescription, matchState, verbose);
}
}
class _EventListCriticalFieldsMatcher extends Matcher {
_EventListCriticalFieldsMatcher(this._expected);
final Iterable<BaseEventMatcher> _expected;
@override
bool matches(dynamic untypedItem, Map<dynamic, dynamic> matchState) {
if (untypedItem is! Iterable<PointerEvent>) {
return false;
}
final Iterable<PointerEvent> item = untypedItem;
final Iterator<PointerEvent> iterator = item.iterator;
if (item.length != _expected.length) {
return false;
}
int i = 0;
for (final BaseEventMatcher matcher in _expected) {
iterator.moveNext();
final Map<dynamic, dynamic> subState = <dynamic, dynamic>{};
final PointerEvent actual = iterator.current;
if (!matcher.matches(actual, subState)) {
addStateInfo(matchState, <dynamic, dynamic>{
'index': i,
'expected': matcher.expected,
'actual': actual,
'matcher': matcher,
'state': subState,
});
return false;
}
i++;
}
return true;
}
@override
Description describe(Description description) {
return description
.add('event list (critical fields only) ')
.addDescriptionOf(_expected);
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
if (item is! Iterable<PointerEvent>) {
return mismatchDescription
.add('is type ${item.runtimeType} instead of Iterable<PointerEvent>');
} else if (item.length != _expected.length) {
return mismatchDescription
.add('has length ${item.length} instead of ${_expected.length}');
} else if (matchState['matcher'] == null) {
return mismatchDescription
.add('met unexpected fatal error');
} else {
mismatchDescription
.add('has\n ')
.addDescriptionOf(matchState['actual'])
.add("\nat index ${matchState['index']}, which doesn't match\n ")
.addDescriptionOf(matchState['expected'])
.add('\nsince it ');
final Description subDescription = StringDescription();
final Matcher matcher = matchState['matcher'] as Matcher;
matcher.describeMismatch(
matchState['actual'],
subDescription,
matchState['state'] as Map<dynamic, dynamic>,
verbose,
);
mismatchDescription.add(subDescription.toString());
return mismatchDescription;
}
}
}
Matcher _equalToEventsOnCriticalFields(List<BaseEventMatcher> source) {
return _EventListCriticalFieldsMatcher(source);
}