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 46 47 48 49 50 51 52 53 54 55 56
      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);
      },
57
    );
58
    setUpMouseAnnotationFinder(
59
      (Offset position) sync* {
60
        yield TestAnnotationEntry(oneAnnotation);
61 62
      },
    );
63 64 65 66
    return oneAnnotation;
  }

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

72
  setUp(() {
73
    binding.postFrameCallbacks.clear();
74 75
  });

76 77
  final Matrix4 translate10by20 = Matrix4.translationValues(10, 20, 0);

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

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

    expect(_mouseTracker.mouseIsConnected, isFalse);

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

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

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

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

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

136 137 138 139 140 141 142 143 144 145
  // 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();
  });

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

    expect(_mouseTracker.mouseIsConnected, isFalse);

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

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

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

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

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

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

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

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

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

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

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

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

    isInHitRegion = false;

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

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

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

292
    // Removing an annotation should trigger events.
293
    isInHitRegion = false;
294 295
    binding.scheduleMouseTrackerPostFrameCheck();
    expect(binding.postFrameCallbacks, hasLength(1));
296

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

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

    isInHitRegion = false;

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

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

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

339
    expect(binding.postFrameCallbacks, hasLength(0));
340 341 342

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

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

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

    isInHitRegion = false;

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

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

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

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

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

412
    expect(binding.postFrameCallbacks, hasLength(0));
413 414 415 416
    events.clear();

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

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

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

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

448
    expect(binding.postFrameCallbacks, hasLength(0));
449 450
  });

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

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

475
    // Passes if no errors are thrown.
476 477
  });

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

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

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

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

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

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

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

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

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

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

606 607 608
class BaseEventMatcher extends Matcher {
  BaseEventMatcher(this.expected)
    : assert(expected != null);
609

610
  final PointerEvent expected;
611

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

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

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

660
class EventMatcher<T extends PointerEvent> extends BaseEventMatcher {
661
  EventMatcher(T super.expected);
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 688 689

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

690 691 692
class _EventListCriticalFieldsMatcher extends Matcher {
  _EventListCriticalFieldsMatcher(this._expected);

693
  final Iterable<BaseEventMatcher> _expected;
694 695 696 697 698

  @override
  bool matches(dynamic untypedItem, Map<dynamic, dynamic> matchState) {
    if (untypedItem is! Iterable<PointerEvent>)
      return false;
699
    final Iterable<PointerEvent> item = untypedItem;
700 701 702 703
    final Iterator<PointerEvent> iterator = item.iterator;
    if (item.length != _expected.length)
      return false;
    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);
}