tooltip_test.dart 66.1 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/foundation.dart';
8
import 'package:flutter/gestures.dart';
Hixie's avatar
Hixie committed
9 10
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
11 12
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
Hixie's avatar
Hixie committed
13

14
import '../foundation/leak_tracking.dart';
15
import '../rendering/mock_canvas.dart';
16
import '../widgets/semantics_tester.dart';
17
import 'feedback_tester.dart';
Hixie's avatar
Hixie committed
18

19 20
const String tooltipText = 'TIP';

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

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

64 65 66 67 68 69 70 71
    /********************* 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)
     *                   *
     *********************/
72

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

83
  testWidgets('Does tooltip end up in the right place - center with padding outside overlay', (WidgetTester tester) async {
84
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
    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(
100
                          key: tooltipKey,
101 102 103 104 105
                          message: tooltipText,
                          height: 20.0,
                          padding: const EdgeInsets.all(5.0),
                          verticalOffset: 20.0,
                          preferBelow: false,
106
                          child: const SizedBox.shrink(),
107 108 109 110 111 112 113 114 115 116 117
                        ),
                      ),
                    ],
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
118
    tooltipKey.currentState?.ensureTooltipVisible();
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
    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);
  });

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

177 178 179 180 181 182 183 184
    /********************* 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
185

186 187 188
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
189
    expect(tip.size.height, equals(24.0)); // 14.0 height + 5.0 padding * 2 (top, bottom)
190
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)), equals(const Offset(10.0, 20.0)));
191
  });
Hixie's avatar
Hixie committed
192

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

228
    /********************* 800x600 screen
229
     *        ___        * }- 10.0 margin
230 231 232 233 234 235 236
     *       |___|       * }-100.0 height
     *         |         * }-100.0 vertical offset
     *         o         * y=300.0
     *                   *
     *                   *
     *                   *
     *********************/
Hixie's avatar
Hixie committed
237

238 239 240
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
241
    expect(tip.size.height, equals(100.0));
242 243
    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
244 245
  });

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

281 282
    // we try to put it here but it doesn't fit:
    /********************* 800x600 screen
283 284
     *        ___        * }- 10.0 margin
     *       |___|       * }-190.0 height (starts at y=9.0)
285 286 287 288 289 290
     *         |         * }-100.0 vertical offset
     *         o         * y=299.0
     *                   *
     *                   *
     *                   *
     *********************/
Hixie's avatar
Hixie committed
291

292 293 294 295 296 297
    // so we put it here:
    /********************* 800x600 screen
     *                   *
     *                   *
     *         o         * y=299.0
     *        _|_        * }-100.0 vertical offset
298 299
     *       |___|       * }-190.0 height
     *                   * }- 10.0 margin
300
     *********************/
Hixie's avatar
Hixie committed
301

302 303 304
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
305
    expect(tip.size.height, equals(190.0));
306 307
    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
308 309
  });

310
  testWidgets('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async {
311
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
312
    await tester.pumpWidget(
313
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
314
        textDirection: TextDirection.ltr,
315
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
316
          initialEntries: <OverlayEntry>[
317
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
318
              builder: (BuildContext context) {
319
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
320
                  children: <Widget>[
321
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
322 323
                      left: 400.0,
                      top: 300.0,
324
                      child: Tooltip(
325
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
326 327
                        message: tooltipText,
                        height: 190.0,
328
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
329 330
                        verticalOffset: 100.0,
                        preferBelow: true,
331
                        child: const SizedBox.shrink(),
Ian Hickson's avatar
Ian Hickson committed
332 333 334 335 336 337 338 339 340
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
341
    );
342
    tooltipKey.currentState?.ensureTooltipVisible();
343
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
344

345 346 347 348 349
    /********************* 800x600 screen
     *                   *
     *                   *
     *         o         * y=300.0
     *        _|_        * }-100.0 vertical offset
350 351
     *       |___|       * }-190.0 height
     *                   * }- 10.0 margin
352
     *********************/
Hixie's avatar
Hixie committed
353

354 355 356
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
357
    expect(tip.size.height, equals(190.0));
358 359
    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
360 361
  });

362
  testWidgets('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async {
363
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
364
    await tester.pumpWidget(
365
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
366
        textDirection: TextDirection.ltr,
367
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
368
          initialEntries: <OverlayEntry>[
369
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
370
              builder: (BuildContext context) {
371
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
372
                  children: <Widget>[
373
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
374 375
                      left: 1600.0,
                      top: 300.0,
376
                      child: Tooltip(
377
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
378 379
                        message: tooltipText,
                        height: 10.0,
380
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
381 382
                        verticalOffset: 10.0,
                        preferBelow: true,
383
                        child: const SizedBox.shrink(),
Ian Hickson's avatar
Ian Hickson committed
384 385 386 387 388 389 390 391 392
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
393
    );
394
    tooltipKey.currentState?.ensureTooltipVisible();
395
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
396

397 398 399 400 401 402 403 404 405
    /********************* 800x600 screen
     *                   *
     *                   *
     *                   * y=300.0;   target -->   o
     *              ___| * }-10.0 vertical offset
     *             |___| * }-10.0 height
     *                   *
     *                   * }-10.0 margin
     *********************/
Hixie's avatar
Hixie committed
406

407 408 409
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
410
    expect(tip.size.height, equals(14.0));
411 412
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0));
413
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0));
414
  });
Hixie's avatar
Hixie committed
415

416
  testWidgets('Does tooltip end up in the right place - near the edge', (WidgetTester tester) async {
417
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
418
    await tester.pumpWidget(
419
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
420
        textDirection: TextDirection.ltr,
421
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
422
          initialEntries: <OverlayEntry>[
423
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
424
              builder: (BuildContext context) {
425
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
426
                  children: <Widget>[
427
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
428 429
                      left: 780.0,
                      top: 300.0,
430
                      child: Tooltip(
431
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
432 433
                        message: tooltipText,
                        height: 10.0,
434
                        padding: EdgeInsets.zero,
Ian Hickson's avatar
Ian Hickson committed
435 436
                        verticalOffset: 10.0,
                        preferBelow: true,
437
                        child: const SizedBox.shrink(),
Ian Hickson's avatar
Ian Hickson committed
438 439 440 441 442 443 444 445 446
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
447
    );
448
    tooltipKey.currentState?.ensureTooltipVisible();
449
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
450

451 452 453 454 455 456 457 458 459
    /********************* 800x600 screen
     *                   *
     *                   *
     *                o  * y=300.0
     *              __|  * }-10.0 vertical offset
     *             |___| * }-10.0 height
     *                   *
     *                   * }-10.0 margin
     *********************/
Hixie's avatar
Hixie committed
460

461 462 463
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
464
    expect(tip.size.height, equals(14.0));
465 466
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0));
467
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0));
468
  });
Hixie's avatar
Hixie committed
469

470 471 472 473 474 475 476 477 478 479 480
  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),
        ),
      );
481 482 483 484 485 486 487
      return MediaQuery(
        data: MediaQueryData(
          viewInsets: EdgeInsets.only(bottom: viewInsetsHeight),
        ),
        child: MaterialApp(
          useInheritedMediaQuery: true,
          home: scaffold,
488 489 490 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
        ),
      );
    }

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

523
  testWidgets('Custom tooltip margin', (WidgetTester tester) async {
524
    const double customMarginValue = 10.0;
525
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
526 527 528 529 530 531 532 533
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
534
                  key: tooltipKey,
535
                  message: tooltipText,
536
                  padding: EdgeInsets.zero,
537
                  margin: const EdgeInsets.all(customMarginValue),
538
                  child: const SizedBox.shrink(),
539 540 541 542 543 544 545
                );
              },
            ),
          ],
        ),
      ),
    );
546
    tooltipKey.currentState?.ensureTooltipVisible();
547 548 549 550 551 552
    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));
553 554
    expect(topLeftTooltipContentInGlobal.dx, topLeftTipInGlobal.dx + customMarginValue);
    expect(topLeftTooltipContentInGlobal.dy, topLeftTipInGlobal.dy + customMarginValue);
555 556 557 558 559

    final Offset topRightTipInGlobal = tester.getTopRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset topRightTooltipContentInGlobal = tester.getTopRight(find.text(tooltipText));
560 561
    expect(topRightTooltipContentInGlobal.dx, topRightTipInGlobal.dx - customMarginValue);
    expect(topRightTooltipContentInGlobal.dy, topRightTipInGlobal.dy + customMarginValue);
562 563 564 565 566

    final Offset bottomLeftTipInGlobal = tester.getBottomLeft(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomLeftTooltipContentInGlobal = tester.getBottomLeft(find.text(tooltipText));
567 568
    expect(bottomLeftTooltipContentInGlobal.dx, bottomLeftTipInGlobal.dx + customMarginValue);
    expect(bottomLeftTooltipContentInGlobal.dy, bottomLeftTipInGlobal.dy - customMarginValue);
569 570 571 572 573

    final Offset bottomRightTipInGlobal = tester.getBottomRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomRightTooltipContentInGlobal = tester.getBottomRight(find.text(tooltipText));
574 575
    expect(bottomRightTooltipContentInGlobal.dx, bottomRightTipInGlobal.dx - customMarginValue);
    expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - customMarginValue);
576 577
  });

578
  testWidgets('Default tooltip message textStyle - light', (WidgetTester tester) async {
579
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
580 581
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
582
        key: tooltipKey,
583 584 585 586 587 588 589 590
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
591
    tooltipKey.currentState?.ensureTooltipVisible();
592 593
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

594
    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
595 596 597
    expect(textStyle.color, Colors.white);
    expect(textStyle.fontFamily, 'Roboto');
    expect(textStyle.decoration, TextDecoration.none);
598
    expect(textStyle.debugLabel, '((englishLike bodyMedium 2014).merge(blackMountainView bodyMedium)).copyWith');
599 600 601
  });

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

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

  testWidgets('Custom tooltip message textStyle', (WidgetTester tester) async {
628
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
629 630
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
631
        key: tooltipKey,
632 633
        textStyle: const TextStyle(
          color: Colors.orange,
634
          decoration: TextDecoration.underline,
635 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 651 652
    expect(textStyle.color, Colors.orange);
    expect(textStyle.fontFamily, null);
    expect(textStyle.decoration, TextDecoration.underline);
  });

653
  testWidgets('Custom tooltip message textAlign', (WidgetTester tester) async {
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669
    await withFlutterLeakTracking(
      () 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],
                ),
              ),
670
            ),
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690
          );
          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);
      },
      tester: tester,
    );
691 692
  });

693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725
  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);
  });

726 727 728 729
  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.
730
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
731 732
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
733
        key: tooltipKey,
734 735 736 737 738 739 740 741
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
742
    tooltipKey.currentState?.ensureTooltipVisible();
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758
    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));
  });

759
  testWidgets('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async {
760
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
761 762 763 764 765 766 767 768
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
769
                  key: tooltipKey,
770
                  message: tooltipText,
771
                  child: const SizedBox.shrink(),
772 773 774 775 776 777 778
                );
              },
            ),
          ],
        ),
      ),
    );
779
    tooltipKey.currentState?.ensureTooltipVisible();
780 781
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

782
    final RenderBox tip = tester.renderObject(_findTooltipContainer(tooltipText));
783 784 785 786 787 788
    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),
    ));
789 790 791

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

794 795
  testWidgets('Tooltip default size, shape, and color test for Desktop', (WidgetTester tester) async {
    // Regressing test for https://github.com/flutter/flutter/issues/68601
796
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
797 798 799
    await tester.pumpWidget(
      MaterialApp(
        home: Tooltip(
800
          key: tooltipKey,
801
          message: tooltipText,
802
          child: const SizedBox.shrink(),
803 804 805
        ),
      ),
    );
806
    tooltipKey.currentState?.ensureTooltipVisible();
807 808 809
    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));
810
    expect(tooltipRenderParagraph.textSize.height, equals(12.0));
811

812 813 814 815
    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)),
816 817
      color: const Color(0xe6616161),
    ));
818 819 820

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

823
  testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async {
824
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
825 826 827 828 829 830 831 832 833 834 835 836
    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(
837
                  key: tooltipKey,
838 839
                  decoration: customDecoration,
                  message: tooltipText,
840
                  child: const SizedBox.shrink(),
841 842 843 844 845 846 847
                );
              },
            ),
          ],
        ),
      ),
    );
848
    tooltipKey.currentState?.ensureTooltipVisible();
849 850
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

851 852 853
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
854 855
    expect(tip.size.height, equals(32.0));
    expect(tip.size.width, equals(74.0));
856
    expect(tip, paints..rrect(color: const Color(0x80800000)));
857
  });
858

859
  testWidgets('Tooltip stays after long press', (WidgetTester tester) async {
860
    await tester.pumpWidget(
861 862 863
      MaterialApp(
        home: Center(
          child: Tooltip(
864
            message: tooltipText,
865
            child: Container(
866 867
              width: 100.0,
              height: 100.0,
868
              color: Colors.green[500],
869 870 871
            ),
          ),
        ),
872
      ),
873 874
    );

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

    // long press reveals tooltip
879 880
    await tester.pump(kLongPressTimeout);
    await tester.pump(const Duration(milliseconds: 10));
881
    expect(find.text(tooltipText), findsOneWidget);
882 883 884 885
    await gesture.up();

    // tap (down, up) gesture hides tooltip, since its not
    // a long press
886 887
    await tester.tap(tooltip);
    await tester.pump(const Duration(milliseconds: 10));
888 889 890
    expect(find.text(tooltipText), findsNothing);

    // long press once more
891 892 893
    gesture = await tester.startGesture(tester.getCenter(tooltip));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 300));
894
    expect(find.text(tooltipText), findsNothing);
895

896
    await tester.pump(kLongPressTimeout);
897
    await tester.pump(const Duration(milliseconds: 10));
898
    expect(find.text(tooltipText), findsOneWidget);
899 900

    // keep holding the long press, should still show tooltip
901
    await tester.pump(kLongPressTimeout);
902
    expect(find.text(tooltipText), findsOneWidget);
903
    await gesture.up();
904 905
  });

906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931
  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);

932
    await _testGestureTap(tester, tooltip);
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
    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);
  });

972 973 974 975 976
  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 {
977
      if (gesture != null) {
978
        return gesture.removePointer();
979
      }
980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028
    });
    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;
  });

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 1056 1057 1058 1059
  testWidgets('Calling ensureTooltipVisible on an unmounted TooltipState returns false', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/95851
    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: Tooltip(
            message: tooltipText,
            child: SizedBox(
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
      ),
    );

    final TooltipState tooltipState = tester.state(find.byType(Tooltip));
    expect(tooltipState.ensureTooltipVisible(), true);

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

    expect(tooltipState.ensureTooltipVisible(), false);
  });

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

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

    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();
1109
    gesture = null;
1110 1111 1112
    expect(find.text(tooltipText), findsNothing);
  });

1113 1114 1115 1116
  testWidgets('Tooltip text is also hoverable', (WidgetTester tester) async {
    const Duration waitDuration = Duration.zero;
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
1117
      gesture?.removePointer();
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 1163 1164 1165 1166
    });
    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);
  });

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 1227 1228 1229 1230
  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);
  });

1231 1232 1233 1234
  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 {
1235
      if (gesture != null) {
1236
        return gesture.removePointer();
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 1276 1277 1278 1279
    });
    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 {
1280
      if (gesture != null) {
1281
        return gesture.removePointer();
1282
      }
1283 1284 1285 1286 1287 1288 1289
    });
    await gesture.addPointer();
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
1290
      const MaterialApp(
1291 1292
        home: Center(
          child: Column(
1293
            children: <Widget>[
1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305
              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'),
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 1339 1340 1341 1342
            ],
          ),
        ),
      ),
    );

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

1343 1344 1345 1346 1347
  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 {
1348
      return gesture.removePointer();
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 1387 1388
    });
    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);
  });

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

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

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

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

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

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

1437
    expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
1438 1439

    semantics.dispose();
Hixie's avatar
Hixie committed
1440
  });
1441 1442 1443

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

    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);
1463
    await tester.tapAt(const Offset(5.0, 5.0));
1464 1465 1466 1467 1468 1469 1470
    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);
  });

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

    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)));
1503 1504 1505
    RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
1506 1507 1508 1509 1510 1511
    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)));
1512 1513 1514
    tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
1515
    expect(tip.size.height, equals(64.0));
1516
  });
1517

1518
  testWidgets('Tooltip text displays with richMessage', (WidgetTester tester) async {
1519
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
1520 1521 1522 1523 1524
    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(
1525
          key: tooltipKey,
1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541
          richMessage: const TextSpan(
            text: textSpan1Text,
            children: <InlineSpan>[
              TextSpan(
                text: textSpan2Text,
              ),
            ],
          ),
          child: Container(
            width: 100.0,
            height: 100.0,
            color: Colors.green[500],
          ),
        ),
      ),
    );
1542
    tooltipKey.currentState?.ensureTooltipVisible();
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 1573 1574
    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>()),
    );
  });

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

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

    feedback.dispose();
  });

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

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

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

    semantics.dispose();
  });

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

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

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

    semantics.dispose();
  });

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

    await tester.pumpWidget(
1687 1688 1689
      MaterialApp(
        home: Center(
          child: Tooltip(
1690
            message: 'Foo',
1691
            child: Container(
1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706
              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',
1707
        'nodeId': _findDebugSemantics(object).id,
1708 1709 1710 1711 1712 1713 1714 1715 1716 1717
        'data': <String, dynamic>{},
      },
      <String, dynamic>{
        'type': 'tooltip',
        'data': <String, dynamic>{
          'message': 'Foo',
        },
      },
    ]));
    semantics.dispose();
1718
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
1719
  });
1720 1721 1722
  testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

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

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

    expect(description, <String>[
      '"message"',
    ]);
  });
1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754
  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"',
    ]);
  });
1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765
  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,
1766
      margin: EdgeInsets.all(5.0),
1767 1768 1769 1770
      height: 100.0,
      excludeFromSemantics: true,
      preferBelow: false,
      verticalOffset: 50.0,
1771 1772
      triggerMode: TooltipTriggerMode.manual,
      enableFeedback: true,
1773 1774 1775 1776 1777 1778 1779 1780 1781 1782
    ).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',
1783
      'margin: EdgeInsets.all(5.0)',
1784 1785 1786 1787 1788
      'vertical offset: 50.0',
      'position: above',
      'semantics: excluded',
      'wait duration: 0:00:01.000000',
      'show duration: 0:00:02.000000',
1789 1790
      'triggerMode: TooltipTriggerMode.manual',
      'enableFeedback: true',
1791 1792
    ]);
  });
1793 1794 1795 1796 1797 1798 1799

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

1800
    await _testGestureTap(tester, tooltip);
1801 1802 1803 1804 1805 1806 1807 1808 1809
    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);

1810
    await _testGestureTap(tester, tooltip);
1811 1812
    expect(find.text(tooltipText), findsNothing);

1813
    await _testGestureLongPress(tester, tooltip);
1814 1815 1816 1817 1818 1819 1820 1821 1822
    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);

1823
    await _testGestureTap(tester, tooltip);
1824 1825 1826 1827 1828 1829 1830 1831 1832
    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);

1833
    await _testGestureTap(tester, tooltip);
1834 1835
    expect(find.text(tooltipText), findsNothing);

1836
    await _testGestureLongPress(tester, tooltip);
1837 1838
    expect(find.text(tooltipText), findsNothing);
  });
1839

1840 1841 1842 1843 1844 1845
  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);
1846
    await _testGestureLongPress(tester, tooltip);
1847 1848 1849 1850 1851
    expect(onTriggeredCalled, true);

    onTriggeredCalled = false;
    await setWidgetForTooltipMode(tester, TooltipTriggerMode.tap, onTriggered: onTriggered);
    tooltip = find.byType(Tooltip);
1852
    await _testGestureTap(tester, tooltip);
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 1887 1888
    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);
  });

1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913
  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);
    }
  });
1914 1915
}

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

1935
Future<void> _testGestureLongPress(WidgetTester tester, Finder tooltip) async {
1936 1937 1938 1939 1940 1941 1942
  final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip));
  await tester.pump();
  await tester.pump(kLongPressTimeout);
  await gestureLongPress.up();
  await tester.pump();
}

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

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