tooltip_test.dart 66 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hixie's avatar
Hixie committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui';

7
import 'package:flutter/gestures.dart';
Hixie's avatar
Hixie committed
8 9
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
10 11
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
Hixie's avatar
Hixie committed
12

13
import '../rendering/mock_canvas.dart';
14
import '../widgets/semantics_tester.dart';
15
import 'feedback_tester.dart';
Hixie's avatar
Hixie committed
16

17 18
const String tooltipText = 'TIP';

19 20 21 22 23 24 25
Finder _findTooltipContainer(String tooltipText) {
  return find.ancestor(
    of: find.text(tooltipText),
    matching: find.byType(Container),
  );
}

Hixie's avatar
Hixie committed
26
void main() {
27
  testWidgets('Does tooltip end up in the right place - center', (WidgetTester tester) async {
28
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
29
    await tester.pumpWidget(
30
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
31
        textDirection: TextDirection.ltr,
32
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
33
          initialEntries: <OverlayEntry>[
34
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
35
              builder: (BuildContext context) {
36
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
37
                  children: <Widget>[
38
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
39 40
                      left: 300.0,
                      top: 0.0,
41
                      child: Tooltip(
42
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
43 44 45 46 47
                        message: tooltipText,
                        height: 20.0,
                        padding: const EdgeInsets.all(5.0),
                        verticalOffset: 20.0,
                        preferBelow: false,
48
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
49 50 51 52 53 54 55 56 57 58 59 60
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
61
    );
62
    tooltipKey.currentState?.ensureTooltipVisible();
63
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
64

65 66 67 68 69 70 71 72
    /********************* 800x600 screen
     *      o            * y=0
     *      |            * }- 20.0 vertical offset, of which 10.0 is in the screen edge margin
     *   +----+          * \- (5.0 padding in height)
     *   |    |          * |- 20 height
     *   +----+          * /- (5.0 padding in height)
     *                   *
     *********************/
73

74 75 76
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
77
    final Offset tipInGlobal = tip.localToGlobal(tip.size.topCenter(Offset.zero));
78 79
    // The exact position of the left side depends on the font the test framework
    // happens to pick, so we don't test that.
80 81
    expect(tipInGlobal.dx, 300.0);
    expect(tipInGlobal.dy, 20.0);
82 83
  });

84
  testWidgets('Does tooltip end up in the right place - center with padding outside overlay', (WidgetTester tester) async {
85
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Overlay(
            initialEntries: <OverlayEntry>[
              OverlayEntry(
                builder: (BuildContext context) {
                  return Stack(
                    children: <Widget>[
                      Positioned(
                        left: 300.0,
                        top: 0.0,
                        child: Tooltip(
101
                          key: tooltipKey,
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
                          message: tooltipText,
                          height: 20.0,
                          padding: const EdgeInsets.all(5.0),
                          verticalOffset: 20.0,
                          preferBelow: false,
                          child: const SizedBox(
                            width: 0.0,
                            height: 0.0,
                          ),
                        ),
                      ),
                    ],
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
122
    tooltipKey.currentState?.ensureTooltipVisible();
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    /************************ 800x600 screen
     *   ________________   * }- 20.0 padding outside overlay
     *  |    o           |  * y=0
     *  |    |           |  * }- 20.0 vertical offset, of which 10.0 is in the screen edge margin
     *  | +----+         |  * \- (5.0 padding in height)
     *  | |    |         |  * |- 20 height
     *  | +----+         |  * /- (5.0 padding in height)
     *  |________________|  *
     *                      * } - 20.0 padding outside overlay
     ************************/

    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
    final Offset tipInGlobal = tip.localToGlobal(tip.size.topCenter(Offset.zero));
    // The exact position of the left side depends on the font the test framework
    // happens to pick, so we don't test that.
    expect(tipInGlobal.dx, 320.0);
    expect(tipInGlobal.dy, 40.0);
  });

146
  testWidgets('Does tooltip end up in the right place - top left', (WidgetTester tester) async {
147
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
148
    await tester.pumpWidget(
149
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
150
        textDirection: TextDirection.ltr,
151
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
152
          initialEntries: <OverlayEntry>[
153
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
154
              builder: (BuildContext context) {
155
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
156
                  children: <Widget>[
157
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
158 159
                      left: 0.0,
                      top: 0.0,
160
                      child: Tooltip(
161
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
162 163 164 165 166
                        message: tooltipText,
                        height: 20.0,
                        padding: const EdgeInsets.all(5.0),
                        verticalOffset: 20.0,
                        preferBelow: false,
167
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
168 169 170 171 172 173 174 175 176 177 178 179
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
180
    );
181
    tooltipKey.currentState?.ensureTooltipVisible();
182
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
183

184 185 186 187 188 189 190 191
    /********************* 800x600 screen
     *o                  * y=0
     *|                  * }- 20.0 vertical offset, of which 10.0 is in the screen edge margin
     *+----+             * \- (5.0 padding in height)
     *|    |             * |- 20 height
     *+----+             * /- (5.0 padding in height)
     *                   *
     *********************/
Hixie's avatar
Hixie committed
192

193 194 195
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
196
    expect(tip.size.height, equals(24.0)); // 14.0 height + 5.0 padding * 2 (top, bottom)
197
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)), equals(const Offset(10.0, 20.0)));
198
  });
Hixie's avatar
Hixie committed
199

200
  testWidgets('Does tooltip end up in the right place - center prefer above fits', (WidgetTester tester) async {
201
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
202
    await tester.pumpWidget(
203
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
204
        textDirection: TextDirection.ltr,
205
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
206
          initialEntries: <OverlayEntry>[
207
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
208
              builder: (BuildContext context) {
209
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
210
                  children: <Widget>[
211
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
212 213
                      left: 400.0,
                      top: 300.0,
214
                      child: Tooltip(
215
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
216 217
                        message: tooltipText,
                        height: 100.0,
218
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
219 220
                        verticalOffset: 100.0,
                        preferBelow: false,
221
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
222 223 224 225 226 227 228 229 230 231 232 233
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
234
    );
235
    tooltipKey.currentState?.ensureTooltipVisible();
236
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
237

238
    /********************* 800x600 screen
239
     *        ___        * }- 10.0 margin
240 241 242 243 244 245 246
     *       |___|       * }-100.0 height
     *         |         * }-100.0 vertical offset
     *         o         * y=300.0
     *                   *
     *                   *
     *                   *
     *********************/
Hixie's avatar
Hixie committed
247

248 249 250
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
251
    expect(tip.size.height, equals(100.0));
252 253
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(100.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(200.0));
Hixie's avatar
Hixie committed
254 255
  });

256
  testWidgets('Does tooltip end up in the right place - center prefer above does not fit', (WidgetTester tester) async {
257
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
258
    await tester.pumpWidget(
259
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
260
        textDirection: TextDirection.ltr,
261
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
262
          initialEntries: <OverlayEntry>[
263
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
264
              builder: (BuildContext context) {
265
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
266
                  children: <Widget>[
267
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
268 269
                      left: 400.0,
                      top: 299.0,
270
                      child: Tooltip(
271
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
272 273
                        message: tooltipText,
                        height: 190.0,
274
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
275 276
                        verticalOffset: 100.0,
                        preferBelow: false,
277
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
278 279 280 281 282 283 284 285 286 287 288 289
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
290
    );
291
    tooltipKey.currentState?.ensureTooltipVisible();
292
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
293

294 295
    // we try to put it here but it doesn't fit:
    /********************* 800x600 screen
296 297
     *        ___        * }- 10.0 margin
     *       |___|       * }-190.0 height (starts at y=9.0)
298 299 300 301 302 303
     *         |         * }-100.0 vertical offset
     *         o         * y=299.0
     *                   *
     *                   *
     *                   *
     *********************/
Hixie's avatar
Hixie committed
304

305 306 307 308 309 310
    // so we put it here:
    /********************* 800x600 screen
     *                   *
     *                   *
     *         o         * y=299.0
     *        _|_        * }-100.0 vertical offset
311 312
     *       |___|       * }-190.0 height
     *                   * }- 10.0 margin
313
     *********************/
Hixie's avatar
Hixie committed
314

315 316 317
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
318
    expect(tip.size.height, equals(190.0));
319 320
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(399.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(589.0));
Hixie's avatar
Hixie committed
321 322
  });

323
  testWidgets('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async {
324
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
325
    await tester.pumpWidget(
326
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
327
        textDirection: TextDirection.ltr,
328
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
329
          initialEntries: <OverlayEntry>[
330
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
331
              builder: (BuildContext context) {
332
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
333
                  children: <Widget>[
334
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
335 336
                      left: 400.0,
                      top: 300.0,
337
                      child: Tooltip(
338
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
339 340
                        message: tooltipText,
                        height: 190.0,
341
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
342 343
                        verticalOffset: 100.0,
                        preferBelow: true,
344
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
345 346 347 348 349 350 351 352 353 354 355 356
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
357
    );
358
    tooltipKey.currentState?.ensureTooltipVisible();
359
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
360

361 362 363 364 365
    /********************* 800x600 screen
     *                   *
     *                   *
     *         o         * y=300.0
     *        _|_        * }-100.0 vertical offset
366 367
     *       |___|       * }-190.0 height
     *                   * }- 10.0 margin
368
     *********************/
Hixie's avatar
Hixie committed
369

370 371 372
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
373
    expect(tip.size.height, equals(190.0));
374 375
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(400.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(590.0));
Hixie's avatar
Hixie committed
376 377
  });

378
  testWidgets('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async {
379
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
380
    await tester.pumpWidget(
381
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
382
        textDirection: TextDirection.ltr,
383
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
384
          initialEntries: <OverlayEntry>[
385
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
386
              builder: (BuildContext context) {
387
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
388
                  children: <Widget>[
389
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
390 391
                      left: 1600.0,
                      top: 300.0,
392
                      child: Tooltip(
393
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
394 395
                        message: tooltipText,
                        height: 10.0,
396
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
397 398
                        verticalOffset: 10.0,
                        preferBelow: true,
399
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
400 401 402 403 404 405 406 407 408 409 410 411
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
412
    );
413
    tooltipKey.currentState?.ensureTooltipVisible();
414
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
415

416 417 418 419 420 421 422 423 424
    /********************* 800x600 screen
     *                   *
     *                   *
     *                   * y=300.0;   target -->   o
     *              ___| * }-10.0 vertical offset
     *             |___| * }-10.0 height
     *                   *
     *                   * }-10.0 margin
     *********************/
Hixie's avatar
Hixie committed
425

426 427 428
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
429
    expect(tip.size.height, equals(14.0));
430 431
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0));
432
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0));
433
  });
Hixie's avatar
Hixie committed
434

435
  testWidgets('Does tooltip end up in the right place - near the edge', (WidgetTester tester) async {
436
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
437
    await tester.pumpWidget(
438
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
439
        textDirection: TextDirection.ltr,
440
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
441
          initialEntries: <OverlayEntry>[
442
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
443
              builder: (BuildContext context) {
444
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
445
                  children: <Widget>[
446
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
447 448
                      left: 780.0,
                      top: 300.0,
449
                      child: Tooltip(
450
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
451 452
                        message: tooltipText,
                        height: 10.0,
453
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
454 455
                        verticalOffset: 10.0,
                        preferBelow: true,
456
                        child: const SizedBox(
Ian Hickson's avatar
Ian Hickson committed
457 458 459 460 461 462 463 464 465 466 467 468
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
469
    );
470
    tooltipKey.currentState?.ensureTooltipVisible();
471
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
472

473 474 475 476 477 478 479 480 481
    /********************* 800x600 screen
     *                   *
     *                   *
     *                o  * y=300.0
     *              __|  * }-10.0 vertical offset
     *             |___| * }-10.0 height
     *                   *
     *                   * }-10.0 margin
     *********************/
Hixie's avatar
Hixie committed
482

483 484 485
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
486
    expect(tip.size.height, equals(14.0));
487 488
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0));
489
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0));
490
  });
Hixie's avatar
Hixie committed
491

492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
  testWidgets('Tooltip should be fully visible when MediaQuery.viewInsets > 0', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/23666
    Widget materialAppWithViewInsets(double viewInsetsHeight) {
      final Widget scaffold = Scaffold(
        body: const TextField(),
        floatingActionButton: FloatingActionButton(
          tooltip: tooltipText,
          onPressed: () { /* do nothing */ },
          child: const Icon(Icons.add),
        ),
      );
      return MediaQuery.fromWindow(
        child: MediaQuery(
          data: MediaQueryData(
            viewInsets: EdgeInsets.only(bottom: viewInsetsHeight),
          ),
          child: MaterialApp(
            useInheritedMediaQuery: true,
            home: scaffold,
          ),
        ),
      );
    }

    // Start with MediaQuery.viewInsets.bottom = 0
    await tester.pumpWidget(materialAppWithViewInsets(0));

    // Show FAB tooltip
    final Finder fabFinder = find.byType(FloatingActionButton);
    await tester.longPress(fabFinder);
    await tester.pump(const Duration(milliseconds: 500));
    expect(find.byType(Tooltip), findsOneWidget);

    // FAB tooltip should be above FAB
    RenderBox tip = tester.renderObject(_findTooltipContainer(tooltipText));
    Offset fabTopRight = tester.getTopRight(fabFinder);
    Offset tooltipTopRight = tip.localToGlobal(tip.size.topRight(Offset.zero));
    expect(tooltipTopRight.dy < fabTopRight.dy, true);

    // Simulate Keyboard opening (MediaQuery.viewInsets.bottom = 300))
    await tester.pumpWidget(materialAppWithViewInsets(300));
    await tester.pumpAndSettle();

    // Show FAB tooltip
    await tester.longPress(fabFinder);
    await tester.pump(const Duration(milliseconds: 500));
    expect(find.byType(Tooltip), findsOneWidget);

    // FAB tooltip should still be above FAB
    tip = tester.renderObject(_findTooltipContainer(tooltipText));
    fabTopRight = tester.getTopRight(fabFinder);
    tooltipTopRight = tip.localToGlobal(tip.size.topRight(Offset.zero));
    expect(tooltipTopRight.dy < fabTopRight.dy, true);
  });

547
  testWidgets('Custom tooltip margin', (WidgetTester tester) async {
548
    const double customMarginValue = 10.0;
549
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
550 551 552 553 554 555 556 557
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
558
                  key: tooltipKey,
559
                  message: tooltipText,
560
                  padding: EdgeInsets.zero,
561
                  margin: const EdgeInsets.all(customMarginValue),
562
                  child: const SizedBox(
563 564 565 566 567 568 569 570 571 572
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
573
    tooltipKey.currentState?.ensureTooltipVisible();
574 575 576 577 578 579
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    final Offset topLeftTipInGlobal = tester.getTopLeft(
      _findTooltipContainer(tooltipText),
    );
    final Offset topLeftTooltipContentInGlobal = tester.getTopLeft(find.text(tooltipText));
580 581
    expect(topLeftTooltipContentInGlobal.dx, topLeftTipInGlobal.dx + customMarginValue);
    expect(topLeftTooltipContentInGlobal.dy, topLeftTipInGlobal.dy + customMarginValue);
582 583 584 585 586

    final Offset topRightTipInGlobal = tester.getTopRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset topRightTooltipContentInGlobal = tester.getTopRight(find.text(tooltipText));
587 588
    expect(topRightTooltipContentInGlobal.dx, topRightTipInGlobal.dx - customMarginValue);
    expect(topRightTooltipContentInGlobal.dy, topRightTipInGlobal.dy + customMarginValue);
589 590 591 592 593

    final Offset bottomLeftTipInGlobal = tester.getBottomLeft(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomLeftTooltipContentInGlobal = tester.getBottomLeft(find.text(tooltipText));
594 595
    expect(bottomLeftTooltipContentInGlobal.dx, bottomLeftTipInGlobal.dx + customMarginValue);
    expect(bottomLeftTooltipContentInGlobal.dy, bottomLeftTipInGlobal.dy - customMarginValue);
596 597 598 599 600

    final Offset bottomRightTipInGlobal = tester.getBottomRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomRightTooltipContentInGlobal = tester.getBottomRight(find.text(tooltipText));
601 602
    expect(bottomRightTooltipContentInGlobal.dx, bottomRightTipInGlobal.dx - customMarginValue);
    expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - customMarginValue);
603 604
  });

605
  testWidgets('Default tooltip message textStyle - light', (WidgetTester tester) async {
606
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
607 608
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
609
        key: tooltipKey,
610 611 612 613 614 615 616 617
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
618
    tooltipKey.currentState?.ensureTooltipVisible();
619 620
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

621
    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
622 623 624
    expect(textStyle.color, Colors.white);
    expect(textStyle.fontFamily, 'Roboto');
    expect(textStyle.decoration, TextDecoration.none);
625
    expect(textStyle.debugLabel, '((englishLike bodyMedium 2014).merge(blackMountainView bodyMedium)).copyWith');
626 627 628
  });

  testWidgets('Default tooltip message textStyle - dark', (WidgetTester tester) async {
629
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
630 631 632 633 634
    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(
        brightness: Brightness.dark,
      ),
      home: Tooltip(
635
        key: tooltipKey,
636 637 638 639 640 641 642 643
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
644
    tooltipKey.currentState?.ensureTooltipVisible();
645 646
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

647
    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
648 649 650
    expect(textStyle.color, Colors.black);
    expect(textStyle.fontFamily, 'Roboto');
    expect(textStyle.decoration, TextDecoration.none);
651
    expect(textStyle.debugLabel, '((englishLike bodyMedium 2014).merge(whiteMountainView bodyMedium)).copyWith');
652 653 654
  });

  testWidgets('Custom tooltip message textStyle', (WidgetTester tester) async {
655
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
656 657
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
658
        key: tooltipKey,
659 660
        textStyle: const TextStyle(
          color: Colors.orange,
661
          decoration: TextDecoration.underline,
662 663 664 665 666 667 668 669 670
        ),
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
671
    tooltipKey.currentState?.ensureTooltipVisible();
672 673
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

674
    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
675 676 677 678 679
    expect(textStyle.color, Colors.orange);
    expect(textStyle.fontFamily, null);
    expect(textStyle.decoration, TextDecoration.underline);
  });

680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
  testWidgets('Custom tooltip message textAlign', (WidgetTester tester) async {
    Future<void> pumpTooltipWithTextAlign({TextAlign? textAlign}) async {
      final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
      await tester.pumpWidget(
        MaterialApp(
          home: Tooltip(
            key: tooltipKey,
            textAlign: textAlign,
            message: tooltipText,
            child: Container(
              width: 100.0,
              height: 100.0,
              color: Colors.green[500],
            ),
          ),
        ),
      );
      tooltipKey.currentState?.ensureTooltipVisible();
      await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
    }

    // Default value should be TextAlign.start
    await pumpTooltipWithTextAlign();
    TextAlign textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
    expect(textAlign, TextAlign.start);

    await pumpTooltipWithTextAlign(textAlign: TextAlign.center);
    textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
    expect(textAlign, TextAlign.center);

    await pumpTooltipWithTextAlign(textAlign: TextAlign.end);
    textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
    expect(textAlign, TextAlign.end);
  });

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
  testWidgets('Tooltip overlay respects ambient Directionality', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/40702.
    Widget buildApp(String text, TextDirection textDirection) {
      return MaterialApp(
        home: Directionality(
          textDirection: textDirection,
          child: Center(
            child: Tooltip(
              message: text,
              child: Container(
                width: 100.0,
                height: 100.0,
                color: Colors.green[500],
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildApp(tooltipText, TextDirection.rtl));
    await tester.longPress(find.byType(Tooltip));
    expect(find.text(tooltipText), findsOneWidget);
    RenderParagraph tooltipRenderParagraph = tester.renderObject<RenderParagraph>(find.text(tooltipText));
    expect(tooltipRenderParagraph.textDirection, TextDirection.rtl);

    await tester.pumpWidget(buildApp(tooltipText, TextDirection.ltr));
    await tester.longPress(find.byType(Tooltip));
    expect(find.text(tooltipText), findsOneWidget);
    tooltipRenderParagraph = tester.renderObject<RenderParagraph>(find.text(tooltipText));
    expect(tooltipRenderParagraph.textDirection, TextDirection.ltr);
  });

748 749 750 751
  testWidgets('Tooltip overlay wrapped with a non-fallback DefaultTextStyle widget', (WidgetTester tester) async {
    // A Material widget is needed as an ancestor of the Text widget.
    // It is invalid to have text in a Material application that
    // does not have a Material ancestor.
752
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
753 754
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
755
        key: tooltipKey,
756 757 758 759 760 761 762 763
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
764
    tooltipKey.currentState?.ensureTooltipVisible();
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    final TextStyle textStyle = tester.widget<DefaultTextStyle>(
      find.ancestor(
        of: find.text(tooltipText),
        matching: find.byType(DefaultTextStyle),
      ).first,
    ).style;

    // The default fallback text style results in a text with a
    // double underline of Color(0xffffff00).
    expect(textStyle.decoration, isNot(TextDecoration.underline));
    expect(textStyle.decorationColor, isNot(const Color(0xffffff00)));
    expect(textStyle.decorationStyle, isNot(TextDecorationStyle.double));
  });

781
  testWidgets('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async {
782
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
783 784 785 786 787 788 789 790
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
791
                  key: tooltipKey,
792
                  message: tooltipText,
793
                  child: const SizedBox.shrink(),
794 795 796 797 798 799 800
                );
              },
            ),
          ],
        ),
      ),
    );
801
    tooltipKey.currentState?.ensureTooltipVisible();
802 803
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

804
    final RenderBox tip = tester.renderObject(_findTooltipContainer(tooltipText));
805 806 807 808 809 810
    expect(tip.size.height, equals(32.0));
    expect(tip.size.width, equals(74.0));
    expect(tip, paints..rrect(
      rrect: RRect.fromRectAndRadius(tip.paintBounds, const Radius.circular(4.0)),
      color: const Color(0xe6616161),
    ));
811 812 813

    final Container tooltipContainer = tester.firstWidget<Container>(_findTooltipContainer(tooltipText));
    expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0));
814
  });
815

816 817
  testWidgets('Tooltip default size, shape, and color test for Desktop', (WidgetTester tester) async {
    // Regressing test for https://github.com/flutter/flutter/issues/68601
818
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
819 820 821
    await tester.pumpWidget(
      MaterialApp(
        home: Tooltip(
822
          key: tooltipKey,
823
          message: tooltipText,
824
          child: const SizedBox.shrink(),
825 826 827
        ),
      ),
    );
828
    tooltipKey.currentState?.ensureTooltipVisible();
829 830 831
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    final RenderParagraph tooltipRenderParagraph = tester.renderObject<RenderParagraph>(find.text(tooltipText));
832
    expect(tooltipRenderParagraph.textSize.height, equals(12.0));
833

834 835 836 837
    final RenderBox tooltipRenderBox = tester.renderObject(_findTooltipContainer(tooltipText));
    expect(tooltipRenderBox.size.height, equals(24.0));
    expect(tooltipRenderBox, paints..rrect(
      rrect: RRect.fromRectAndRadius(tooltipRenderBox.paintBounds, const Radius.circular(4.0)),
838 839
      color: const Color(0xe6616161),
    ));
840 841 842

    final Container tooltipContainer = tester.firstWidget<Container>(_findTooltipContainer(tooltipText));
    expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0));
843 844
  }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows}));

845
  testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async {
846
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
847 848 849 850 851 852 853 854 855 856 857 858
    const Decoration customDecoration = ShapeDecoration(
      shape: StadiumBorder(),
      color: Color(0x80800000),
    );
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
859
                  key: tooltipKey,
860 861
                  decoration: customDecoration,
                  message: tooltipText,
862
                  child: const SizedBox(
863 864 865 866 867 868 869 870 871 872
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
873
    tooltipKey.currentState?.ensureTooltipVisible();
874 875
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

876 877 878
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
879 880 881 882 883
    expect(tip.size.height, equals(32.0));
    expect(tip.size.width, equals(74.0));
    expect(tip, paints..path(
      color: const Color(0x80800000),
    ));
884
  });
885

886
  testWidgets('Tooltip stays after long press', (WidgetTester tester) async {
887
    await tester.pumpWidget(
888 889 890
      MaterialApp(
        home: Center(
          child: Tooltip(
891
            message: tooltipText,
892
            child: Container(
893 894
              width: 100.0,
              height: 100.0,
895
              color: Colors.green[500],
896 897 898
            ),
          ),
        ),
899
      ),
900 901
    );

902
    final Finder tooltip = find.byType(Tooltip);
903
    TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip));
904 905

    // long press reveals tooltip
906 907
    await tester.pump(kLongPressTimeout);
    await tester.pump(const Duration(milliseconds: 10));
908
    expect(find.text(tooltipText), findsOneWidget);
909 910 911 912
    await gesture.up();

    // tap (down, up) gesture hides tooltip, since its not
    // a long press
913 914
    await tester.tap(tooltip);
    await tester.pump(const Duration(milliseconds: 10));
915 916 917
    expect(find.text(tooltipText), findsNothing);

    // long press once more
918 919 920
    gesture = await tester.startGesture(tester.getCenter(tooltip));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 300));
921
    expect(find.text(tooltipText), findsNothing);
922

923
    await tester.pump(kLongPressTimeout);
924
    await tester.pump(const Duration(milliseconds: 10));
925
    expect(find.text(tooltipText), findsOneWidget);
926 927

    // keep holding the long press, should still show tooltip
928
    await tester.pump(kLongPressTimeout);
929
    expect(find.text(tooltipText), findsOneWidget);
930
    await gesture.up();
931 932
  });

933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
  testWidgets('Tooltip is dismissed after a long press and showDuration expired', (WidgetTester tester) async {
    const Duration showDuration = Duration(seconds: 3);
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress, showDuration: showDuration);

    final Finder tooltip = find.byType(Tooltip);
    final TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip));

    // Long press reveals tooltip
    await tester.pump(kLongPressTimeout);
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text(tooltipText), findsOneWidget);
    await gesture.up();

    // Tooltip is dismissed after showDuration expired
    await tester.pump(showDuration);
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text(tooltipText), findsNothing);
  });

  testWidgets('Tooltip is dismissed after a tap and showDuration expired', (WidgetTester tester) async {
    const Duration showDuration = Duration(seconds: 3);
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, showDuration: showDuration);

    final Finder tooltip = find.byType(Tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureTap(tester, tooltip);
    expect(find.text(tooltipText), findsOneWidget);

    // Tooltip is dismissed after showDuration expired
    await tester.pump(showDuration);
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text(tooltipText), findsNothing);
  });

  testWidgets('Tooltip is dismissed after a tap and showDuration expired when competing with a GestureDetector', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/98854
    const Duration showDuration = Duration(seconds: 3);
    await tester.pumpWidget(
      MaterialApp(
        home: GestureDetector(
          onVerticalDragStart: (_) { /* Do nothing */ },
          child: const Tooltip(
            message: tooltipText,
            triggerMode: TooltipTriggerMode.tap,
            showDuration: showDuration,
            child: SizedBox(width: 100.0, height: 100.0),
          ),
        ),
      ),
    );
    final Finder tooltip = find.byType(Tooltip);
    expect(find.text(tooltipText), findsNothing);

    await tester.tap(tooltip);
    // Wait for GestureArena disambiguation, delay is kPressTimeout to disambiguate
    // between onTap and onVerticalDragStart
    await tester.pump(kPressTimeout);
    expect(find.text(tooltipText), findsOneWidget);

    // Tooltip is dismissed after showDuration expired
    await tester.pump(showDuration);
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text(tooltipText), findsNothing);
  });

999 1000 1001 1002 1003
  testWidgets('Dispatch the mouse events before tip overlay detached', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/96890
    const Duration waitDuration = Duration.zero;
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
1004
      if (gesture != null) {
1005
        return gesture.removePointer();
1006
      }
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055
    });
    await gesture.addPointer();
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
            child: SizedBox(
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
      ),
    );

    // Trigger the tip overlay.
    final Finder tooltip = find.byType(Tooltip);
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();
    // Wait for it to appear.
    await tester.pump(waitDuration);

    // Remove the `Tooltip` widget.
    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: SizedBox.shrink(),
        ),
      ),
    );

    // The tooltip overlay still on the tree and it will removed in the next frame.

    // Dispatch the mouse in and out events before the overlay detached.
    await gesture.moveTo(tester.getCenter(find.text(tooltipText)));
    await gesture.moveTo(Offset.zero);
    await tester.pumpAndSettle();

    // Go without crashes.
    await gesture.removePointer();
    gesture = null;
  });

1056
  testWidgets('Tooltip shows/hides when hovered', (WidgetTester tester) async {
1057
    const Duration waitDuration = Duration.zero;
1058
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
1059
    addTearDown(() async {
1060
      if (gesture != null) {
1061
        return gesture.removePointer();
1062
      }
1063
    });
Michael Goderbauer's avatar
Michael Goderbauer committed
1064
    await gesture.addPointer();
1065 1066 1067 1068 1069
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
1070
      const MaterialApp(
1071 1072 1073 1074
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
1075
            child: SizedBox(
1076 1077 1078 1079 1080
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
1081
      ),
1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
    );

    final Finder tooltip = find.byType(Tooltip);
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();
    // Wait for it to appear.
    await tester.pump(waitDuration);
    expect(find.text(tooltipText), findsOneWidget);

    // Wait a looong time to make sure that it doesn't go away if the mouse is
    // still over the widget.
    await tester.pump(const Duration(days: 1));
    await tester.pumpAndSettle();
    expect(find.text(tooltipText), findsOneWidget);

    await gesture.moveTo(Offset.zero);
    await tester.pump();

    // Wait for it to disappear.
    await tester.pumpAndSettle();
    await gesture.removePointer();
1105
    gesture = null;
1106 1107 1108
    expect(find.text(tooltipText), findsNothing);
  });

1109 1110 1111 1112
  testWidgets('Tooltip text is also hoverable', (WidgetTester tester) async {
    const Duration waitDuration = Duration.zero;
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
1113
      gesture?.removePointer();
1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162
    });
    await gesture.addPointer();
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
            child: Text('I am tool tip'),
          ),
        ),
      ),
    );

    final Finder tooltip = find.byType(Tooltip);
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();
    // Wait for it to appear.
    await tester.pump(waitDuration);
    expect(find.text(tooltipText), findsOneWidget);

    // Wait a looong time to make sure that it doesn't go away if the mouse is
    // still over the widget.
    await tester.pump(const Duration(days: 1));
    await tester.pumpAndSettle();
    expect(find.text(tooltipText), findsOneWidget);

    // Hover to the tool tip text and verify the tooltip doesn't go away.
    await gesture.moveTo(tester.getTopLeft(find.text(tooltipText)));
    await tester.pump(const Duration(days: 1));
    await tester.pumpAndSettle();
    expect(find.text(tooltipText), findsOneWidget);

    await gesture.moveTo(Offset.zero);
    await tester.pump();

    // Wait for it to disappear.
    await tester.pumpAndSettle();
    await gesture.removePointer();
    gesture = null;
    expect(find.text(tooltipText), findsNothing);
  });

1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
  testWidgets('Tooltip should not show more than one tooltip when hovered', (WidgetTester tester) async {
    const Duration waitDuration = Duration(milliseconds: 500);
    final UniqueKey innerKey = UniqueKey();
    final UniqueKey outerKey = UniqueKey();
    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: Tooltip(
            message: 'Outer',
            child: Container(
              key: outerKey,
              width: 100,
              height: 100,
              alignment: Alignment.centerRight,
              child: Tooltip(
                message: 'Inner',
                child: SizedBox(
                  key: innerKey,
                  width: 25,
                  height: 100,
                ),
              ),
            ),
          ),
        ),
      ),
    );

    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async { gesture?.removePointer(); });

    // Both the inner and outer containers have tooltips associated with them, but only
    // the currently hovered one should appear, even though the pointer is inside both.
    final Finder outer = find.byKey(outerKey);
    final Finder inner = find.byKey(innerKey);
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(outer));
    await tester.pump();
    await gesture.moveTo(tester.getCenter(inner));
    await tester.pump();

    // Wait for it to appear.
    await tester.pump(waitDuration);

    expect(find.text('Outer'), findsNothing);
    expect(find.text('Inner'), findsOneWidget);
    await gesture.moveTo(tester.getCenter(outer));
    await tester.pump();
    // Wait for it to switch.
    await tester.pump(waitDuration);
    expect(find.text('Outer'), findsOneWidget);
    expect(find.text('Inner'), findsNothing);

    await gesture.moveTo(Offset.zero);

    // Wait for all tooltips to disappear.
    await tester.pumpAndSettle();
    await gesture.removePointer();
    gesture = null;
    expect(find.text('Outer'), findsNothing);
    expect(find.text('Inner'), findsNothing);
  });

1227 1228 1229 1230
  testWidgets('Tooltip can be dismissed by escape key', (WidgetTester tester) async {
    const Duration waitDuration = Duration.zero;
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
1231
      if (gesture != null) {
1232
        return gesture.removePointer();
1233
      }
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
    });
    await gesture.addPointer();
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
            child: Text('I am tool tip'),
          ),
        ),
      ),
    );

    final Finder tooltip = find.byType(Tooltip);
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();
    // Wait for it to appear.
    await tester.pump(waitDuration);
    expect(find.text(tooltipText), findsOneWidget);

    // Try to dismiss the tooltip with the shortcut key
    await tester.sendKeyEvent(LogicalKeyboardKey.escape);
    await tester.pumpAndSettle();
    expect(find.text(tooltipText), findsNothing);

    await gesture.moveTo(Offset.zero);
    await tester.pumpAndSettle();
    await gesture.removePointer();
    gesture = null;
  });

  testWidgets('Multiple Tooltips are dismissed by escape key', (WidgetTester tester) async {
    const Duration waitDuration = Duration.zero;
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
1276
      if (gesture != null) {
1277
        return gesture.removePointer();
1278
      }
1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301
    });
    await gesture.addPointer();
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: Column(
            children: const <Widget>[
              Tooltip(
                message: 'message1',
                waitDuration: waitDuration,
                showDuration: Duration(days: 1),
                child: Text('tooltip1'),
              ),
              Spacer(flex: 2),
              Tooltip(
                message: 'message2',
                waitDuration: waitDuration,
                showDuration: Duration(days: 1),
                child: Text('tooltip2'),
1302
              ),
1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338
            ],
          ),
        ),
      ),
    );

    final Finder tooltip = find.text('tooltip1');
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();
    await tester.pump(waitDuration);
    expect(find.text('message1'), findsOneWidget);

    final Finder secondTooltip = find.text('tooltip2');
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(secondTooltip));
    await tester.pump();
    await tester.pump(waitDuration);
    // Make sure both messages are on the screen.
    expect(find.text('message1'), findsOneWidget);
    expect(find.text('message2'), findsOneWidget);

    // Try to dismiss the tooltip with the shortcut key
    await tester.sendKeyEvent(LogicalKeyboardKey.escape);
    await tester.pumpAndSettle();
    expect(find.text('message1'), findsNothing);
    expect(find.text('message2'), findsNothing);

    await gesture.moveTo(Offset.zero);
    await tester.pumpAndSettle();
    await gesture.removePointer();
    gesture = null;
  });

1339 1340 1341 1342 1343
  testWidgets('Tooltip does not attempt to show after unmount', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/54096.
    const Duration waitDuration = Duration(seconds: 1);
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
1344
      if (gesture != null) {
1345
        return gesture.removePointer();
1346
      }
1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386
    });
    await gesture.addPointer();
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
            child: SizedBox(
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
      ),
    );

    final Finder tooltip = find.byType(Tooltip);
    await gesture.moveTo(Offset.zero);
    await tester.pump();
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();

    // Pump another random widget to unmount the Tooltip widget.
    await tester.pumpWidget(
        const MaterialApp(
          home: Center(
            child: SizedBox(),
        ),
      ),
    );

    // If the issue regresses, an exception will be thrown while we are waiting.
    await tester.pump(waitDuration);
  });

1387
  testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
1388
    final SemanticsTester semantics = SemanticsTester(tester);
1389
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
1390

1391
    await tester.pumpWidget(
1392
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
1393
        textDirection: TextDirection.ltr,
1394
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
1395
          initialEntries: <OverlayEntry>[
1396
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
1397
              builder: (BuildContext context) {
1398
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
1399
                  children: <Widget>[
1400
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
1401 1402
                      left: 780.0,
                      top: 300.0,
1403
                      child: Tooltip(
1404
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
1405
                        message: tooltipText,
1406
                        child: const SizedBox(width: 10.0, height: 10.0),
Ian Hickson's avatar
Ian Hickson committed
1407 1408 1409 1410 1411 1412 1413 1414 1415
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
1416
    );
1417

1418
    final TestSemantics expected = TestSemantics.root(
1419
      children: <TestSemantics>[
1420
        TestSemantics.rootChild(
1421
          id: 1,
1422
          tooltip: 'TIP',
1423 1424
          textDirection: TextDirection.ltr,
        ),
1425
      ],
1426 1427 1428
    );

    expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
Hixie's avatar
Hixie committed
1429

1430
    // This triggers a rebuild of the semantics because the tree changes.
1431
    tooltipKey.currentState?.ensureTooltipVisible();
Hixie's avatar
Hixie committed
1432

1433
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
1434

1435
    expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
1436 1437

    semantics.dispose();
Hixie's avatar
Hixie committed
1438
  });
1439 1440 1441

  testWidgets('Tooltip overlay does not update', (WidgetTester tester) async {
    Widget buildApp(String text) {
1442 1443 1444
      return MaterialApp(
        home: Center(
          child: Tooltip(
1445
            message: text,
1446
            child: Container(
1447 1448
              width: 100.0,
              height: 100.0,
1449
              color: Colors.green[500],
1450 1451 1452
            ),
          ),
        ),
1453 1454 1455 1456 1457 1458 1459 1460
      );
    }

    await tester.pumpWidget(buildApp(tooltipText));
    await tester.longPress(find.byType(Tooltip));
    expect(find.text(tooltipText), findsOneWidget);
    await tester.pumpWidget(buildApp('NEW'));
    expect(find.text(tooltipText), findsOneWidget);
1461
    await tester.tapAt(const Offset(5.0, 5.0));
1462 1463 1464 1465 1466 1467 1468
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    expect(find.text(tooltipText), findsNothing);
    await tester.longPress(find.byType(Tooltip));
    expect(find.text(tooltipText), findsNothing);
  });

1469
  testWidgets('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
1470
    Widget buildApp(String text, { required double textScaleFactor }) {
1471 1472 1473
      return MediaQuery(
        data: MediaQueryData(textScaleFactor: textScaleFactor),
        child: Directionality(
1474
          textDirection: TextDirection.ltr,
1475
          child: Navigator(
1476
            onGenerateRoute: (RouteSettings settings) {
1477
              return MaterialPageRoute<void>(
1478
                builder: (BuildContext context) {
1479 1480
                  return Center(
                    child: Tooltip(
1481
                      message: text,
1482
                      child: Container(
1483 1484 1485 1486 1487 1488
                        width: 100.0,
                        height: 100.0,
                        color: Colors.green[500],
                      ),
                    ),
                  );
1489
                },
1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildApp(tooltipText, textScaleFactor: 1.0));
    await tester.longPress(find.byType(Tooltip));
    expect(find.text(tooltipText), findsOneWidget);
    expect(tester.getSize(find.text(tooltipText)), equals(const Size(42.0, 14.0)));
1501 1502 1503
    RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
1504 1505 1506 1507 1508 1509
    expect(tip.size.height, equals(32.0));

    await tester.pumpWidget(buildApp(tooltipText, textScaleFactor: 4.0));
    await tester.longPress(find.byType(Tooltip));
    expect(find.text(tooltipText), findsOneWidget);
    expect(tester.getSize(find.text(tooltipText)), equals(const Size(168.0, 56.0)));
1510 1511 1512
    tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
1513
    expect(tip.size.height, equals(64.0));
1514
  });
1515

1516
  testWidgets('Tooltip text displays with richMessage', (WidgetTester tester) async {
1517
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
1518 1519 1520 1521 1522
    const String textSpan1Text = 'I am a rich tooltip message. ';
    const String textSpan2Text = 'I am another span of a rich tooltip message';
    await tester.pumpWidget(
      MaterialApp(
        home: Tooltip(
1523
          key: tooltipKey,
1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539
          richMessage: const TextSpan(
            text: textSpan1Text,
            children: <InlineSpan>[
              TextSpan(
                text: textSpan2Text,
              ),
            ],
          ),
          child: Container(
            width: 100.0,
            height: 100.0,
            color: Colors.green[500],
          ),
        ),
      ),
    );
1540
    tooltipKey.currentState?.ensureTooltipVisible();
1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    final RichText richText = tester.widget<RichText>(find.byType(RichText));
    expect(richText.text.toPlainText(), equals('$textSpan1Text$textSpan2Text'));
  });

  testWidgets('Tooltip throws assertion error when both message and richMessage are specified', (WidgetTester tester) async {
    expect(
      () {
        MaterialApp(
          home: Tooltip(
            message: 'I am a tooltip message.',
            richMessage: const TextSpan(
              text: 'I am a rich tooltip.',
              children: <InlineSpan>[
                TextSpan(
                  text: 'I am another span of a rich tooltip.',
                ),
              ],
            ),
            child: Container(
              width: 100.0,
              height: 100.0,
              color: Colors.green[500],
            ),
          ),
        );
      },
      throwsA(const TypeMatcher<AssertionError>()),
    );
  });

1573
  testWidgets('Haptic feedback', (WidgetTester tester) async {
1574
    final FeedbackTester feedback = FeedbackTester();
Ian Hickson's avatar
Ian Hickson committed
1575
    await tester.pumpWidget(
1576 1577 1578
      MaterialApp(
        home: Center(
          child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
1579
            message: 'Foo',
1580
            child: Container(
Ian Hickson's avatar
Ian Hickson committed
1581 1582 1583 1584 1585 1586 1587
              width: 100.0,
              height: 100.0,
              color: Colors.green[500],
            ),
          ),
        ),
      ),
1588 1589 1590 1591 1592 1593 1594 1595 1596
    );

    await tester.longPress(find.byType(Tooltip));
    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(feedback.hapticCount, 1);

    feedback.dispose();
  });

1597
  testWidgets('Semantics included', (WidgetTester tester) async {
1598
    final SemanticsTester semantics = SemanticsTester(tester);
1599 1600

    await tester.pumpWidget(
1601 1602
      const MaterialApp(
        home: Center(
1603
          child: Tooltip(
1604
            message: 'Foo',
1605
            child: Text('Bar'),
1606 1607 1608 1609 1610
          ),
        ),
      ),
    );

1611
    expect(semantics, hasSemantics(TestSemantics.root(
1612
      children: <TestSemantics>[
1613
        TestSemantics.rootChild(
1614
          children: <TestSemantics>[
1615
            TestSemantics(
1616
              children: <TestSemantics>[
1617
                TestSemantics(
1618 1619 1620
                  flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                  children: <TestSemantics>[
                    TestSemantics(
1621 1622
                      tooltip: 'Foo',
                      label: 'Bar',
1623 1624 1625
                      textDirection: TextDirection.ltr,
                    ),
                  ],
1626
                ),
1627 1628
              ],
            ),
1629
          ],
1630 1631 1632 1633 1634 1635 1636 1637
        ),
      ],
    ), ignoreRect: true, ignoreId: true, ignoreTransform: true));

    semantics.dispose();
  });

  testWidgets('Semantics excluded', (WidgetTester tester) async {
1638
    final SemanticsTester semantics = SemanticsTester(tester);
1639 1640

    await tester.pumpWidget(
1641 1642
      const MaterialApp(
        home: Center(
1643
          child: Tooltip(
1644 1645
            message: 'Foo',
            excludeFromSemantics: true,
1646
            child: Text('Bar'),
1647 1648 1649 1650 1651
          ),
        ),
      ),
    );

1652
    expect(semantics, hasSemantics(TestSemantics.root(
1653
      children: <TestSemantics>[
1654
        TestSemantics.rootChild(
1655
          children: <TestSemantics>[
1656
            TestSemantics(
1657
              children: <TestSemantics>[
1658
                TestSemantics(
1659 1660 1661 1662 1663 1664 1665
                  flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                  children: <TestSemantics>[
                    TestSemantics(
                      label: 'Bar',
                      textDirection: TextDirection.ltr,
                    ),
                  ],
1666
                ),
1667 1668
              ],
            ),
1669
          ],
1670 1671 1672 1673 1674 1675 1676
        ),
      ],
    ), ignoreRect: true, ignoreId: true, ignoreTransform: true));

    semantics.dispose();
  });

1677 1678
  testWidgets('has semantic events', (WidgetTester tester) async {
    final List<dynamic> semanticEvents = <dynamic>[];
1679
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async {
1680 1681
      semanticEvents.add(message);
    });
1682
    final SemanticsTester semantics = SemanticsTester(tester);
1683 1684

    await tester.pumpWidget(
1685 1686 1687
      MaterialApp(
        home: Center(
          child: Tooltip(
1688
            message: 'Foo',
1689
            child: Container(
1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715
              width: 100.0,
              height: 100.0,
              color: Colors.green[500],
            ),
          ),
        ),
      ),
    );

    await tester.longPress(find.byType(Tooltip));
    final RenderObject object = tester.firstRenderObject(find.byType(Tooltip));

    expect(semanticEvents, unorderedEquals(<dynamic>[
      <String, dynamic>{
        'type': 'longPress',
        'nodeId': findDebugSemantics(object).id,
        'data': <String, dynamic>{},
      },
      <String, dynamic>{
        'type': 'tooltip',
        'data': <String, dynamic>{
          'message': 'Foo',
        },
      },
    ]));
    semantics.dispose();
1716
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
1717
  });
1718 1719 1720
  testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

1721
    const Tooltip(message: 'message').debugFillProperties(builder);
1722 1723 1724 1725 1726 1727 1728 1729 1730

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[
      '"message"',
    ]);
  });
1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752
  testWidgets('default Tooltip debugFillProperties with richMessage', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    const Tooltip(
      richMessage: TextSpan(
        text: 'This is a ',
        children: <InlineSpan>[
          TextSpan(
            text: 'richMessage',
          ),
        ],
      ),
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
        .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
        .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[
      '"This is a richMessage"',
    ]);
  });
1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763
  testWidgets('Tooltip implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    // Not checking controller, inputFormatters, focusNode
    const Tooltip(
      key: ValueKey<String>('foo'),
      message: 'message',
      decoration: BoxDecoration(),
      waitDuration: Duration(seconds: 1),
      showDuration: Duration(seconds: 2),
      padding: EdgeInsets.zero,
1764
      margin: EdgeInsets.all(5.0),
1765 1766 1767 1768
      height: 100.0,
      excludeFromSemantics: true,
      preferBelow: false,
      verticalOffset: 50.0,
1769 1770
      triggerMode: TooltipTriggerMode.manual,
      enableFeedback: true,
1771 1772 1773 1774 1775 1776 1777 1778 1779 1780
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[
      '"message"',
      'height: 100.0',
      'padding: EdgeInsets.zero',
1781
      'margin: EdgeInsets.all(5.0)',
1782 1783 1784 1785 1786
      'vertical offset: 50.0',
      'position: above',
      'semantics: excluded',
      'wait duration: 0:00:01.000000',
      'show duration: 0:00:02.000000',
1787 1788
      'triggerMode: TooltipTriggerMode.manual',
      'enableFeedback: true',
1789 1790
    ]);
  });
1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836

  testWidgets('Tooltip triggers on tap when trigger mode is tap', (WidgetTester tester) async {
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap);

    final Finder tooltip = find.byType(Tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureTap(tester, tooltip);
    expect(find.text(tooltipText), findsOneWidget);
  });

  testWidgets('Tooltip triggers on long press when mode is long press', (WidgetTester tester) async {
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress);

    final Finder tooltip = find.byType(Tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureTap(tester, tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureLongPress(tester, tooltip);
    expect(find.text(tooltipText), findsOneWidget);
  });

  testWidgets('Tooltip does not trigger on tap when trigger mode is longPress', (WidgetTester tester) async {
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress);

    final Finder tooltip = find.byType(Tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureTap(tester, tooltip);
    expect(find.text(tooltipText), findsNothing);
  });

  testWidgets('Tooltip does not trigger when trigger mode is manual', (WidgetTester tester) async {
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.manual);

    final Finder tooltip = find.byType(Tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureTap(tester, tooltip);
    expect(find.text(tooltipText), findsNothing);

    await testGestureLongPress(tester, tooltip);
    expect(find.text(tooltipText), findsNothing);
  });
1837

1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886
  testWidgets('Tooltip onTriggered is called when Tooltip triggers', (WidgetTester tester) async {
    bool onTriggeredCalled = false;
    void onTriggered() => onTriggeredCalled = true;

    await setWidgetForTooltipMode(tester, TooltipTriggerMode.longPress, onTriggered: onTriggered);
    Finder tooltip = find.byType(Tooltip);
    await testGestureLongPress(tester, tooltip);
    expect(onTriggeredCalled, true);

    onTriggeredCalled = false;
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, onTriggered: onTriggered);
    tooltip = find.byType(Tooltip);
    await testGestureTap(tester, tooltip);
    expect(onTriggeredCalled, true);
  });

  testWidgets('Tooltip onTriggered is not called when Tooltip is hovered', (WidgetTester tester) async {
    bool onTriggeredCalled = false;
    void onTriggered() => onTriggeredCalled = true;

    const Duration waitDuration = Duration.zero;
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
            onTriggered: onTriggered,
            child: const SizedBox(
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
      ),
    );

    final Finder tooltip = find.byType(Tooltip);
    await gesture.moveTo(tester.getCenter(tooltip));
    await tester.pump();
    // Wait for it to appear.
    await tester.pump(waitDuration);
    expect(onTriggeredCalled, false);
  });

1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911
  testWidgets('Tooltip should not be shown with empty message (with child)', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Tooltip(
          message: tooltipText,
          child: Text(tooltipText),
        ),
      ),
    );
    expect(find.text(tooltipText), findsOneWidget);
  });

  testWidgets('Tooltip should not be shown with empty message (without child)', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Tooltip(
          message: tooltipText,
        ),
      ),
    );
    expect(find.text(tooltipText), findsNothing);
    if (tooltipText.isEmpty) {
      expect(find.byType(SizedBox), findsOneWidget);
    }
  });
1912 1913
}

1914 1915 1916 1917 1918 1919
Future<void> setWidgetForTooltipMode(
  WidgetTester tester,
  TooltipTriggerMode triggerMode, {
  Duration? showDuration,
  TooltipTriggeredCallback? onTriggered,
}) async {
1920 1921 1922 1923 1924
  await tester.pumpWidget(
    MaterialApp(
      home: Tooltip(
        message: tooltipText,
        triggerMode: triggerMode,
1925
        onTriggered: onTriggered,
1926
        showDuration: showDuration,
1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943
        child: const SizedBox(width: 100.0, height: 100.0),
      ),
    ),
  );
}

Future<void> testGestureLongPress(WidgetTester tester, Finder tooltip) async {
  final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip));
  await tester.pump();
  await tester.pump(kLongPressTimeout);
  await gestureLongPress.up();
  await tester.pump();
}

Future<void> testGestureTap(WidgetTester tester, Finder tooltip) async {
  await tester.tap(tooltip);
  await tester.pump(const Duration(milliseconds: 10));
1944 1945 1946
}

SemanticsNode findDebugSemantics(RenderObject object) {
1947
  if (object.debugSemantics != null) {
1948
    return object.debugSemantics!;
1949
  }
1950
  return findDebugSemantics(object.parent! as RenderObject);
Hixie's avatar
Hixie committed
1951
}