Unverified Commit 297fe91f authored by Tong Mu's avatar Tong Mu Committed by GitHub

Refactor: Make MouseTracker test concise with some utility functions (#41791)

* Extract pointerData and _equalToEventsOnCriticalFields for simplicity
parent 6edd79ab
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'dart:ui' show PointerChange;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
......@@ -37,26 +38,22 @@ void ensureTestGestureBinding() {
void main() {
setUp(ensureTestGestureBinding);
final List<PointerEnterEvent> enter = <PointerEnterEvent>[];
final List<PointerHoverEvent> move = <PointerHoverEvent>[];
final List<PointerExitEvent> exit = <PointerExitEvent>[];
final List<PointerEvent> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => enter.add(event),
onHover: (PointerHoverEvent event) => move.add(event),
onExit: (PointerExitEvent event) => exit.add(event),
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
// Only respond to some mouse events.
final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => enter.add(event),
onHover: (PointerHoverEvent event) => move.add(event),
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
);
bool isInHitRegionOne;
bool isInHitRegionTwo;
void clear() {
enter.clear();
exit.clear();
move.clear();
events.clear();
}
setUp(() {
......@@ -80,133 +77,68 @@ void main() {
test('receives and processes mouse hover events', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover, // Will implicitly also add a PointerAdded event.
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
// Will implicitly also add a PointerAdded event.
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
]);
final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 301.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(1.0, 301.0)),
]);
final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 401.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
device: 1,
),
_pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
]);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
isInHitRegionOne = true;
ui.window.onPointerDataPacket(packet1);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(0.0, 0.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(0.0, 0.0)),
]));
clear();
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 101.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerHoverEvent(position: Offset(1.0, 101.0)),
]));
clear();
ui.window.onPointerDataPacket(packet3);
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 201.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(1), reason: 'exit contains $exit');
expect(exit.first.position, equals(const Offset(1.0, 201.0)));
expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerHoverEvent(position: Offset(1.0, 201.0)),
const PointerExitEvent(position: Offset(1.0, 201.0)),
]));
clear();
ui.window.onPointerDataPacket(packet4);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 301.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 301.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 301.0)),
const PointerHoverEvent(position: Offset(1.0, 301.0)),
]));
// add in a second mouse simultaneously.
clear();
ui.window.onPointerDataPacket(packet5);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{1});
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 401.0)));
expect(enter.first.device, equals(1));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(2), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 401.0)));
expect(move.first.device, equals(1));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.last.position, equals(const Offset(1.0, 401.0)));
expect(move.last.device, equals(1));
expect(move.last.runtimeType, equals(PointerHoverEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 401.0), device: 1),
const PointerHoverEvent(position: Offset(1.0, 401.0), device: 1),
const PointerHoverEvent(position: Offset(1.0, 401.0), device: 1),
]));
});
test('detects exit when annotated layer no longer hit', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(1.0, 201.0)),
]);
isInHitRegionOne = true;
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
......@@ -214,29 +146,19 @@ void main() {
ui.window.onPointerDataPacket(packet1);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(2), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(0.0, 0.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.last.position, equals(const Offset(1.0, 101.0)));
expect(move.last.device, equals(0));
expect(move.last.runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(1.0, 101.0)),
]));
// Simulate layer going away by detaching it.
clear();
isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'enter contains $move');
expect(exit.length, equals(1), reason: 'enter contains $exit');
expect(exit.first.position, const Offset(1.0, 201.0));
expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerExitEvent(position: Offset(1.0, 201.0)),
]));
// Actually detach annotation. Shouldn't receive hit.
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
......@@ -244,19 +166,13 @@ void main() {
isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'enter contains $move');
expect(exit.length, equals(0), reason: 'enter contains $exit');
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
});
test("don't flip out if not all mouse events are listened to", () {
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
isInHitRegionOne = false;
......@@ -266,106 +182,207 @@ void main() {
ui.window.onPointerDataPacket(packet);
RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation);
isInHitRegionTwo = false;
// Passes if no errors are thrown
});
test('detects exit when mouse goes away', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
]);
isInHitRegionOne = true;
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet1);
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.delta, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(3), reason: 'move contains $move');
expect(move[0].position, equals(const Offset(0.0, 0.0)));
expect(move[0].delta, equals(const Offset(0.0, 0.0)));
expect(move[0].device, equals(0));
expect(move[0].runtimeType, equals(PointerHoverEvent));
expect(move[1].position, equals(const Offset(1.0, 101.0)));
expect(move[1].delta, equals(const Offset(1.0, 101.0)));
expect(move[1].device, equals(0));
expect(move[1].runtimeType, equals(PointerHoverEvent));
expect(move[2].position, equals(const Offset(1.0, 201.0)));
expect(move[2].delta, equals(const Offset(0.0, 100.0)));
expect(move[2].device, equals(0));
expect(move[2].runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(1), reason: 'exit contains $exit');
expect(exit.first.position, equals(const Offset(1.0, 201.0)));
expect(exit.first.delta, equals(const Offset(0.0, 0.0)));
expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(1.0, 101.0)),
const PointerHoverEvent(position: Offset(1.0, 201.0)),
const PointerExitEvent(position: Offset(1.0, 201.0)),
]));
});
test('handles mouse down and move', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.hover,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.down,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 101.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 1.0 * ui.window.devicePixelRatio,
physicalY: 201.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
),
_pointerData(PointerChange.down, const Offset(1.0, 101.0)),
_pointerData(PointerChange.move, const Offset(1.0, 201.0)),
]);
isInHitRegionOne = true;
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet1);
ui.window.onPointerDataPacket(packet2);
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(2), reason: 'move contains $move');
expect(move[0].position, equals(const Offset(0.0, 0.0)));
expect(move[0].device, equals(0));
expect(move[0].runtimeType, equals(PointerHoverEvent));
expect(move[1].position, equals(const Offset(1.0, 101.0)));
expect(move[1].device, equals(0));
expect(move[1].runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
const PointerHoverEvent(position: Offset(1.0, 101.0), delta: Offset(1.0, 101.0)),
]));
});
}
ui.PointerData _pointerData(
PointerChange change,
Offset logicalPosition, {
int device = 0,
}) {
return ui.PointerData(
change: change,
physicalX: logicalPosition.dx * ui.window.devicePixelRatio,
physicalY: logicalPosition.dy * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
device: device,
);
}
class _EventCriticalFieldsMatcher extends Matcher {
_EventCriticalFieldsMatcher(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) {
if (untypedItem.runtimeType != _expected.runtimeType) {
return false;
}
final PointerEvent actual = untypedItem;
if (!(
_matchesField(matchState, 'kind', actual.kind, PointerDeviceKind.mouse) &&
_matchesField(matchState, 'position', actual.position, _expected.position) &&
_matchesField(matchState, 'device', actual.device, _expected.device)
)) {
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,
) {
if (item.runtimeType != _expected.runtimeType) {
return mismatchDescription
.add('is ')
.addDescriptionOf(item.runtimeType)
.add(' and doesn\'t match ')
.addDescriptionOf(_expected.runtimeType);
}
return mismatchDescription
.add('has ')
.addDescriptionOf(matchState['actual'])
.add(' at field `${matchState['field']}`, which doesn\'t match the expected ')
.addDescriptionOf(matchState['expected']);
}
}
class _EventListCriticalFieldsMatcher extends Matcher {
_EventListCriticalFieldsMatcher(this._expected);
final Iterable<PointerEvent> _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 PointerEvent e in _expected) {
iterator.moveNext();
final Matcher matcher = _EventCriticalFieldsMatcher(e);
final Map<dynamic, dynamic> subState = <dynamic, dynamic>{};
final PointerEvent actual = iterator.current;
if (!matcher.matches(actual, subState)) {
addStateInfo(matchState, <dynamic, dynamic>{
'index': i,
'expected': e,
'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'];
matcher.describeMismatch(matchState['actual'], subDescription,
matchState['state'], verbose);
mismatchDescription.add(subDescription.toString());
return mismatchDescription;
}
}
}
Matcher _equalToEventsOnCriticalFields(List<PointerEvent> source) {
return _EventListCriticalFieldsMatcher(source);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment