mouse_tracker_test.dart 29.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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;
6
import 'dart:ui' show PointerChange;
7 8

import 'package:flutter/gestures.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter_test/flutter_test.dart';
11

12
import 'mouse_tracker_test_utils.dart';
13

14
MouseTracker get _mouseTracker => RendererBinding.instance.mouseTracker;
15

16
typedef SimpleAnnotationFinder = Iterable<TestAnnotationEntry> Function(Offset offset);
17

18
void main() {
19
  final TestMouseTrackerFlutterBinding binding = TestMouseTrackerFlutterBinding();
20
  void setUpMouseAnnotationFinder(SimpleAnnotationFinder annotationFinder) {
21
    binding.setHitTest((BoxHitTestResult result, Offset position) {
22 23 24
      for (final TestAnnotationEntry entry in annotationFinder(position)) {
        result.addWithRawTransform(
          transform: entry.transform,
25
          position: position,
26 27 28 29 30 31 32 33
          hitTest: (BoxHitTestResult result, Offset position) {
            result.add(entry);
            return true;
          },
        );
      }
      return true;
    });
34
  }
35

36 37 38 39 40
  // 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`.
41
  TestAnnotationTarget setUpWithOneAnnotation({
42
    required List<PointerEvent> logEvents,
43
  }) {
44
    final TestAnnotationTarget oneAnnotation = TestAnnotationTarget(
45
      onEnter: (PointerEnterEvent event) {
46
        logEvents.add(event);
47 48
      },
      onHover: (PointerHoverEvent event) {
49
        logEvents.add(event);
50 51
      },
      onExit: (PointerExitEvent event) {
52
        logEvents.add(event);
53
      },
54
    );
55
    setUpMouseAnnotationFinder(
56
      (Offset position) sync* {
57
        yield TestAnnotationEntry(oneAnnotation);
58 59
      },
    );
60 61 62 63
    return oneAnnotation;
  }

  void dispatchRemoveDevice([int device = 0]) {
64
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
65
      _pointerData(PointerChange.remove, Offset.zero, device: device),
66
    ]));
67
  }
68

69
  setUp(() {
70
    binding.postFrameCallbacks.clear();
71 72
  });

73 74
  final Matrix4 translate10by20 = Matrix4.translationValues(10, 20, 0);

75 76
  test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
    final List<PointerEvent> events = <PointerEvent>[];
77
    setUpWithOneAnnotation(logEvents: events);
78 79 80 81 82 83 84 85

    final List<bool> listenerLogs = <bool>[];
    _mouseTracker.addListener(() {
      listenerLogs.add(_mouseTracker.mouseIsConnected);
    });

    expect(_mouseTracker.mouseIsConnected, isFalse);

86
    // Pointer enters the annotation.
87
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
88
      _pointerData(PointerChange.add, Offset.zero),
89
    ]));
90
    addTearDown(() => dispatchRemoveDevice());
91

92
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
93
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent()),
94
    ]));
95 96 97
    expect(listenerLogs, <bool>[true]);
    events.clear();
    listenerLogs.clear();
98

99
    // Pointer hovers the annotation.
100
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
101 102
      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
    ]));
103 104
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 101.0))),
105
    ]));
106
    expect(_mouseTracker.mouseIsConnected, isTrue);
107
    expect(listenerLogs, isEmpty);
108
    events.clear();
109

110
    // Pointer is removed while on the annotation.
111
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
112
      _pointerData(PointerChange.remove, const Offset(1.0, 101.0)),
113
    ]));
114 115
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(1.0, 101.0))),
116
    ]));
117 118 119
    expect(listenerLogs, <bool>[false]);
    events.clear();
    listenerLogs.clear();
120

121
    // Pointer is added on the annotation.
122
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
123
      _pointerData(PointerChange.add, const Offset(0.0, 301.0)),
124
    ]));
125 126
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 301.0))),
127
    ]));
128 129 130 131 132
    expect(listenerLogs, <bool>[true]);
    events.clear();
    listenerLogs.clear();
  });

133 134 135 136
  // 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);
137
    binding.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
138 139 140 141 142
      _pointerData(PointerChange.remove, Offset.zero),
    ]));
    events.clear();
  });

143 144
  test('should correctly handle multiple devices', () {
    final List<PointerEvent> events = <PointerEvent>[];
145
    setUpWithOneAnnotation(logEvents: events);
146 147 148

    expect(_mouseTracker.mouseIsConnected, isFalse);

149
    // The first mouse is added on the annotation.
150
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
151
      _pointerData(PointerChange.add, Offset.zero),
152 153
      _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
    ]));
154
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
155
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent()),
156
      EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(0.0, 1.0))),
157 158 159
    ]));
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();
160

161
    // The second mouse is added on the annotation.
162
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
163
      _pointerData(PointerChange.add, const Offset(0.0, 401.0), device: 1),
164 165
      _pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
    ]));
166 167 168
    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)),
169
    ]));
170 171 172
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();

173
    // The first mouse moves on the annotation.
174
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
175 176
      _pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
    ]));
177 178
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(0.0, 101.0))),
179 180 181 182
    ]));
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();

183
    // The second mouse moves on the annotation.
184
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
185 186
      _pointerData(PointerChange.hover, const Offset(1.0, 501.0), device: 1),
    ]));
187 188
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 501.0), device: 1)),
189 190 191 192
    ]));
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();

193
    // The first mouse is removed while on the annotation.
194
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
195 196
      _pointerData(PointerChange.remove, const Offset(0.0, 101.0)),
    ]));
197 198
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 101.0))),
199 200 201 202
    ]));
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();

203
    // The second mouse still moves on the annotation.
204
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
205 206
      _pointerData(PointerChange.hover, const Offset(1.0, 601.0), device: 1),
    ]));
207 208
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerHoverEvent>(const PointerHoverEvent(position: Offset(1.0, 601.0), device: 1)),
209 210 211 212
    ]));
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();

213
    // The second mouse is removed while on the annotation.
214
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
215 216
      _pointerData(PointerChange.remove, const Offset(1.0, 601.0), device: 1),
    ]));
217 218
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(1.0, 601.0), device: 1)),
219 220 221
    ]));
    expect(_mouseTracker.mouseIsConnected, isFalse);
    events.clear();
222
  });
223

224 225
  test('should not handle non-hover events', () {
    final List<PointerEvent> events = <PointerEvent>[];
226
    setUpWithOneAnnotation(logEvents: events);
227

228
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
229
      _pointerData(PointerChange.add, const Offset(0.0, 101.0)),
230 231
      _pointerData(PointerChange.down, const Offset(0.0, 101.0)),
    ]));
232
    addTearDown(() => dispatchRemoveDevice());
233
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
234 235
      // This Enter event is triggered by the [PointerAddedEvent] The
      // [PointerDownEvent] is ignored by [MouseTracker].
236
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 101.0))),
237
    ]));
238
    events.clear();
239

240
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
241 242
      _pointerData(PointerChange.move, const Offset(0.0, 201.0)),
    ]));
243
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
244
    events.clear();
245

246
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
247 248
      _pointerData(PointerChange.up, const Offset(0.0, 301.0)),
    ]));
249
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
250 251 252
    events.clear();
  });

253
  test('should correctly handle when the annotation appears or disappears on the pointer', () {
254
    late bool isInHitRegion;
255
    final List<Object> events = <PointerEvent>[];
256
    final TestAnnotationTarget annotation = TestAnnotationTarget(
257 258 259 260
      onEnter: (PointerEnterEvent event) => events.add(event),
      onHover: (PointerHoverEvent event) => events.add(event),
      onExit: (PointerExitEvent event) => events.add(event),
    );
261
    setUpMouseAnnotationFinder((Offset position) sync* {
262
      if (isInHitRegion) {
263
        yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
264 265 266 267 268
      }
    });

    isInHitRegion = false;

269
    // Connect a mouse when there is no annotation.
270
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
271 272
      _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
    ]));
273
    addTearDown(() => dispatchRemoveDevice());
274
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[]));
275 276 277
    expect(_mouseTracker.mouseIsConnected, isTrue);
    events.clear();

278
    // Adding an annotation should trigger Enter event.
279
    isInHitRegion = true;
280 281
    binding.scheduleMouseTrackerPostFrameCheck();
    expect(binding.postFrameCallbacks, hasLength(1));
282

283
    binding.flushPostFrameCallbacks(Duration.zero);
284 285
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0, 100)).transformed(translate10by20)),
286 287 288
    ]));
    events.clear();

289
    // Removing an annotation should trigger events.
290
    isInHitRegion = false;
291 292
    binding.scheduleMouseTrackerPostFrameCheck();
    expect(binding.postFrameCallbacks, hasLength(1));
293

294
    binding.flushPostFrameCallbacks(Duration.zero);
295 296
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
297
    ]));
298
    expect(binding.postFrameCallbacks, hasLength(0));
299 300 301
  });

  test('should correctly handle when the annotation moves in or out of the pointer', () {
302
    late bool isInHitRegion;
303
    final List<Object> events = <PointerEvent>[];
304
    final TestAnnotationTarget annotation = TestAnnotationTarget(
305 306 307 308
      onEnter: (PointerEnterEvent event) => events.add(event),
      onHover: (PointerHoverEvent event) => events.add(event),
      onExit: (PointerExitEvent event) => events.add(event),
    );
309
    setUpMouseAnnotationFinder((Offset position) sync* {
310
      if (isInHitRegion) {
311
        yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
312 313 314 315 316 317
      }
    });

    isInHitRegion = false;

    // Connect a mouse.
318
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
319 320
      _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
    ]));
321
    addTearDown(() => dispatchRemoveDevice());
322 323 324 325
    events.clear();

    // During a frame, the annotation moves into the pointer.
    isInHitRegion = true;
326 327 328
    expect(binding.postFrameCallbacks, hasLength(0));
    binding.scheduleMouseTrackerPostFrameCheck();
    expect(binding.postFrameCallbacks, hasLength(1));
329

330
    binding.flushPostFrameCallbacks(Duration.zero);
331 332
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
333 334 335
    ]));
    events.clear();

336
    expect(binding.postFrameCallbacks, hasLength(0));
337 338 339

    // During a frame, the annotation moves out of the pointer.
    isInHitRegion = false;
340 341 342
    expect(binding.postFrameCallbacks, hasLength(0));
    binding.scheduleMouseTrackerPostFrameCheck();
    expect(binding.postFrameCallbacks, hasLength(1));
343

344
    binding.flushPostFrameCallbacks(Duration.zero);
345 346
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
347
    ]));
348
    expect(binding.postFrameCallbacks, hasLength(0));
349 350
  });

351
  test('should correctly handle when the pointer is added or removed on the annotation', () {
352
    late bool isInHitRegion;
353
    final List<Object> events = <PointerEvent>[];
354
    final TestAnnotationTarget annotation = TestAnnotationTarget(
355 356 357 358
      onEnter: (PointerEnterEvent event) => events.add(event),
      onHover: (PointerHoverEvent event) => events.add(event),
      onExit: (PointerExitEvent event) => events.add(event),
    );
359
    setUpMouseAnnotationFinder((Offset position) sync* {
360
      if (isInHitRegion) {
361
        yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
362 363 364 365 366 367 368
      }
    });

    isInHitRegion = false;

    // Connect a mouse in the region. Should trigger Enter.
    isInHitRegion = true;
369
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
370 371 372
      _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
    ]));

373
    expect(binding.postFrameCallbacks, hasLength(0));
374 375
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerEnterEvent>(const PointerEnterEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
376 377 378 379
    ]));
    events.clear();

    // Disconnect the mouse from the region. Should trigger Exit.
380
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
381 382
      _pointerData(PointerChange.remove, const Offset(0.0, 100.0)),
    ]));
383
    expect(binding.postFrameCallbacks, hasLength(0));
384 385
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(0.0, 100.0)).transformed(translate10by20)),
386 387 388 389
    ]));
  });

  test('should correctly handle when the pointer moves in or out of the annotation', () {
390
    late bool isInHitRegion;
391
    final List<Object> events = <PointerEvent>[];
392
    final TestAnnotationTarget annotation = TestAnnotationTarget(
393 394 395 396
      onEnter: (PointerEnterEvent event) => events.add(event),
      onHover: (PointerHoverEvent event) => events.add(event),
      onExit: (PointerExitEvent event) => events.add(event),
    );
397
    setUpMouseAnnotationFinder((Offset position) sync* {
398
      if (isInHitRegion) {
399
        yield TestAnnotationEntry(annotation, Matrix4.translationValues(10, 20, 0));
400 401 402 403
      }
    });

    isInHitRegion = false;
404
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
405 406
      _pointerData(PointerChange.add, const Offset(200.0, 100.0)),
    ]));
407
    addTearDown(() => dispatchRemoveDevice());
408

409
    expect(binding.postFrameCallbacks, hasLength(0));
410 411 412 413
    events.clear();

    // Moves the mouse into the region. Should trigger Enter.
    isInHitRegion = true;
414
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
415 416
      _pointerData(PointerChange.hover, const Offset(0.0, 100.0)),
    ]));
417
    expect(binding.postFrameCallbacks, hasLength(0));
418 419 420
    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)),
421 422 423 424 425
    ]));
    events.clear();

    // Moves the mouse out of the region. Should trigger Exit.
    isInHitRegion = false;
426
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
427 428
      _pointerData(PointerChange.hover, const Offset(200.0, 100.0)),
    ]));
429
    expect(binding.postFrameCallbacks, hasLength(0));
430 431
    expect(events, _equalToEventsOnCriticalFields(<BaseEventMatcher>[
      EventMatcher<PointerExitEvent>(const PointerExitEvent(position: Offset(200.0, 100.0)).transformed(translate10by20)),
432 433 434
    ]));
  });

435
  test('should not schedule post-frame callbacks when no mouse is connected', () {
436
    setUpMouseAnnotationFinder((Offset position) sync* {
437 438
    });

439
    // Connect a touch device, which should not be recognized by MouseTracker
440
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
441 442 443 444
      _pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch),
    ]));
    expect(_mouseTracker.mouseIsConnected, isFalse);

445
    expect(binding.postFrameCallbacks, hasLength(0));
446 447
  });

448 449 450
  test('should not flip out if not all mouse events are listened to', () {
    bool isInHitRegionOne = true;
    bool isInHitRegionTwo = false;
451
    final TestAnnotationTarget annotation1 = TestAnnotationTarget(
452
      onEnter: (PointerEnterEvent event) {},
453
    );
454
    final TestAnnotationTarget annotation2 = TestAnnotationTarget(
455
      onExit: (PointerExitEvent event) {},
456
    );
457
    setUpMouseAnnotationFinder((Offset position) sync* {
458
      if (isInHitRegionOne) {
459
        yield TestAnnotationEntry(annotation1);
460
      } else if (isInHitRegionTwo) {
461
        yield TestAnnotationEntry(annotation2);
462
      }
463 464
    });

465 466
    isInHitRegionOne = false;
    isInHitRegionTwo = true;
467
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
468
      _pointerData(PointerChange.add, const Offset(0.0, 101.0)),
469
      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
470
    ]));
471
    addTearDown(() => dispatchRemoveDevice());
472

473
    // Passes if no errors are thrown.
474 475
  });

476 477 478 479 480 481 482 483 484 485
  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   | |
    //   |  —————— |
    //   ———————————

486
    late bool isInB;
487
    final List<String> logs = <String>[];
488
    final TestAnnotationTarget annotationA = TestAnnotationTarget(
489 490 491 492
      onEnter: (PointerEnterEvent event) => logs.add('enterA'),
      onExit: (PointerExitEvent event) => logs.add('exitA'),
      onHover: (PointerHoverEvent event) => logs.add('hoverA'),
    );
493
    final TestAnnotationTarget annotationB = TestAnnotationTarget(
494 495 496 497
      onEnter: (PointerEnterEvent event) => logs.add('enterB'),
      onExit: (PointerExitEvent event) => logs.add('exitB'),
      onHover: (PointerHoverEvent event) => logs.add('hoverB'),
    );
498
    setUpMouseAnnotationFinder((Offset position) sync* {
499
      // Children's annotations come before parents'.
500
      if (isInB) {
501 502
        yield TestAnnotationEntry(annotationB);
        yield TestAnnotationEntry(annotationA);
503 504 505
      }
    });

506
    // Starts out of A.
507
    isInB = false;
508
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
509
      _pointerData(PointerChange.add, const Offset(0.0, 1.0)),
510
    ]));
511
    addTearDown(() => dispatchRemoveDevice());
512 513
    expect(logs, <String>[]);

514
    // Moves into B within one frame.
515
    isInB = true;
516
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
517 518
      _pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
    ]));
519
    expect(logs, <String>['enterA', 'enterB', 'hoverB', 'hoverA']);
520 521
    logs.clear();

522
    // Moves out of A within one frame.
523
    isInB = false;
524
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
525 526 527 528 529 530 531 532 533 534 535 536 537 538
      _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     |
    //   |      |  |      |
    //   ————————  ————————

539 540
    late bool isInA;
    late bool isInB;
541
    final List<String> logs = <String>[];
542
    final TestAnnotationTarget annotationA = TestAnnotationTarget(
543 544 545 546
      onEnter: (PointerEnterEvent event) => logs.add('enterA'),
      onExit: (PointerExitEvent event) => logs.add('exitA'),
      onHover: (PointerHoverEvent event) => logs.add('hoverA'),
    );
547
    final TestAnnotationTarget annotationB = TestAnnotationTarget(
548 549 550 551
      onEnter: (PointerEnterEvent event) => logs.add('enterB'),
      onExit: (PointerExitEvent event) => logs.add('exitB'),
      onHover: (PointerHoverEvent event) => logs.add('hoverB'),
    );
552
    setUpMouseAnnotationFinder((Offset position) sync* {
553
      if (isInA) {
554
        yield TestAnnotationEntry(annotationA);
555
      } else if (isInB) {
556
        yield TestAnnotationEntry(annotationB);
557 558 559
      }
    });

560
    // Starts within A.
561 562
    isInA = true;
    isInB = false;
563
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
564
      _pointerData(PointerChange.add, const Offset(0.0, 1.0)),
565
    ]));
566
    addTearDown(() => dispatchRemoveDevice());
567
    expect(logs, <String>['enterA']);
568 569
    logs.clear();

570
    // Moves into B within one frame.
571 572
    isInA = false;
    isInB = true;
573
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
574 575 576 577 578
      _pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
    ]));
    expect(logs, <String>['exitA', 'enterB', 'hoverB']);
    logs.clear();

579
    // Moves into A within one frame.
580 581
    isInA = true;
    isInB = false;
582
    RendererBinding.instance.platformDispatcher.onPointerDataPacket!(ui.PointerDataPacket(data: <ui.PointerData>[
583
      _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
584
    ]));
585
    expect(logs, <String>['exitB', 'enterA', 'hoverA']);
586 587
  });
}
588 589 590 591 592

ui.PointerData _pointerData(
  PointerChange change,
  Offset logicalPosition, {
  int device = 0,
593
  PointerDeviceKind kind = PointerDeviceKind.mouse,
594
}) {
595
  final double devicePixelRatio = RendererBinding.instance.platformDispatcher.implicitView!.devicePixelRatio;
596 597
  return ui.PointerData(
    change: change,
598 599
    physicalX: logicalPosition.dx * devicePixelRatio,
    physicalY: logicalPosition.dy * devicePixelRatio,
600
    kind: kind,
601 602 603 604
    device: device,
  );
}

605
class BaseEventMatcher extends Matcher {
606
  BaseEventMatcher(this.expected);
607

608
  final PointerEvent expected;
609

610
  bool _matchesField(Map<dynamic, dynamic> matchState, String field, dynamic actual, dynamic expected) {
611 612 613 614 615 616 617 618 619 620 621 622 623
    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) {
624
    final PointerEvent actual = untypedItem as PointerEvent;
625 626
    if (!(
      _matchesField(matchState, 'kind', actual.kind, PointerDeviceKind.mouse) &&
627 628 629
      _matchesField(matchState, 'position', actual.position, expected.position) &&
      _matchesField(matchState, 'device', actual.device, expected.device) &&
      _matchesField(matchState, 'localPosition', actual.localPosition, expected.localPosition)
630 631 632 633 634 635 636 637 638 639
    )) {
      return false;
    }
    return true;
  }

  @override
  Description describe(Description description) {
    return description
      .add('event (critical fields only) ')
640
      .addDescriptionOf(expected);
641 642 643 644 645 646 647 648 649 650 651 652
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    return mismatchDescription
      .add('has ')
      .addDescriptionOf(matchState['actual'])
653
      .add(" at field `${matchState['field']}`, which doesn't match the expected ")
654 655 656 657
      .addDescriptionOf(matchState['expected']);
  }
}

658
class EventMatcher<T extends PointerEvent> extends BaseEventMatcher {
659
  EventMatcher(T super.expected);
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687

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

688 689 690
class _EventListCriticalFieldsMatcher extends Matcher {
  _EventListCriticalFieldsMatcher(this._expected);

691
  final Iterable<BaseEventMatcher> _expected;
692 693 694

  @override
  bool matches(dynamic untypedItem, Map<dynamic, dynamic> matchState) {
695
    if (untypedItem is! Iterable<PointerEvent>) {
696
      return false;
697
    }
698
    final Iterable<PointerEvent> item = untypedItem;
699
    final Iterator<PointerEvent> iterator = item.iterator;
700
    if (item.length != _expected.length) {
701
      return false;
702
    }
703
    int i = 0;
704
    for (final BaseEventMatcher matcher in _expected) {
705 706 707 708 709 710
      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,
711
          'expected': matcher.expected,
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
          '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'])
750
        .add("\nat index ${matchState['index']}, which doesn't match\n  ")
751 752 753
        .addDescriptionOf(matchState['expected'])
        .add('\nsince it ');
      final Description subDescription = StringDescription();
754
      final Matcher matcher = matchState['matcher'] as Matcher;
755 756 757 758 759 760
      matcher.describeMismatch(
        matchState['actual'],
        subDescription,
        matchState['state'] as Map<dynamic, dynamic>,
        verbose,
      );
761 762 763 764 765 766
      mismatchDescription.add(subDescription.toString());
      return mismatchDescription;
    }
  }
}

767
Matcher _equalToEventsOnCriticalFields(List<BaseEventMatcher> source) {
768 769
  return _EventListCriticalFieldsMatcher(source);
}