mirror of
https://github.com/flutter/flutter.git
synced 2026-01-23 16:36:37 +08:00
760 lines
28 KiB
Dart
760 lines
28 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 '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]) {
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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();
|
|
});
|
|
|
|
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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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);
|
|
|
|
ui.window.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();
|
|
|
|
ui.window.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
|
|
_pointerData(PointerChange.move, const Offset(0.0, 201.0)),
|
|
]));
|
|
expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
|
|
events.clear();
|
|
|
|
ui.window.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.
|
|
ui.window.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.
|
|
ui.window.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;
|
|
ui.window.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.
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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;
|
|
ui.window.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 * ui.window.devicePixelRatio,
|
|
physicalY: logicalPosition.dy * ui.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 expected) : 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);
|
|
}
|