tooltip_test.dart 57.9 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
  testWidgets('Custom tooltip margin', (WidgetTester tester) async {
493
    const double customMarginValue = 10.0;
494
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
495 496 497 498 499 500 501 502
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
503
                  key: tooltipKey,
504
                  message: tooltipText,
505
                  padding: EdgeInsets.zero,
506
                  margin: const EdgeInsets.all(customMarginValue),
507
                  child: const SizedBox(
508 509 510 511 512 513 514 515 516 517
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
518
    tooltipKey.currentState?.ensureTooltipVisible();
519 520 521 522 523 524
    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));
525 526
    expect(topLeftTooltipContentInGlobal.dx, topLeftTipInGlobal.dx + customMarginValue);
    expect(topLeftTooltipContentInGlobal.dy, topLeftTipInGlobal.dy + customMarginValue);
527 528 529 530 531

    final Offset topRightTipInGlobal = tester.getTopRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset topRightTooltipContentInGlobal = tester.getTopRight(find.text(tooltipText));
532 533
    expect(topRightTooltipContentInGlobal.dx, topRightTipInGlobal.dx - customMarginValue);
    expect(topRightTooltipContentInGlobal.dy, topRightTipInGlobal.dy + customMarginValue);
534 535 536 537 538

    final Offset bottomLeftTipInGlobal = tester.getBottomLeft(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomLeftTooltipContentInGlobal = tester.getBottomLeft(find.text(tooltipText));
539 540
    expect(bottomLeftTooltipContentInGlobal.dx, bottomLeftTipInGlobal.dx + customMarginValue);
    expect(bottomLeftTooltipContentInGlobal.dy, bottomLeftTipInGlobal.dy - customMarginValue);
541 542 543 544 545

    final Offset bottomRightTipInGlobal = tester.getBottomRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomRightTooltipContentInGlobal = tester.getBottomRight(find.text(tooltipText));
546 547
    expect(bottomRightTooltipContentInGlobal.dx, bottomRightTipInGlobal.dx - customMarginValue);
    expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - customMarginValue);
548 549
  });

550
  testWidgets('Default tooltip message textStyle - light', (WidgetTester tester) async {
551
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
552 553
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
554
        key: tooltipKey,
555 556 557 558 559 560 561 562
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
563
    tooltipKey.currentState?.ensureTooltipVisible();
564 565
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

566
    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
567 568 569
    expect(textStyle.color, Colors.white);
    expect(textStyle.fontFamily, 'Roboto');
    expect(textStyle.decoration, TextDecoration.none);
570
    expect(textStyle.debugLabel, '((englishLike bodyMedium 2014).merge(blackMountainView bodyMedium)).copyWith');
571 572 573
  });

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

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

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

619
    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
620 621 622 623 624
    expect(textStyle.color, Colors.orange);
    expect(textStyle.fontFamily, null);
    expect(textStyle.decoration, TextDecoration.underline);
  });

625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657
  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);
  });

658 659 660 661
  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.
662
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
663 664
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
665
        key: tooltipKey,
666 667 668 669 670 671 672 673
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
674
    tooltipKey.currentState?.ensureTooltipVisible();
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690
    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));
  });

691
  testWidgets('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async {
692
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
693 694 695 696 697 698 699 700
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
701
                  key: tooltipKey,
702
                  message: tooltipText,
703
                  child: const SizedBox(
704 705 706 707 708 709 710 711 712 713
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
714
    tooltipKey.currentState?.ensureTooltipVisible();
715 716
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

717 718 719
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
720 721 722 723 724 725
    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),
    ));
726
  });
727

728 729
  testWidgets('Tooltip default size, shape, and color test for Desktop', (WidgetTester tester) async {
    // Regressing test for https://github.com/flutter/flutter/issues/68601
730
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
731 732 733
    await tester.pumpWidget(
      MaterialApp(
        home: Tooltip(
734
          key: tooltipKey,
735 736 737 738 739 740 741 742
          message: tooltipText,
          child: const SizedBox(
            width: 0.0,
            height: 0.0,
          ),
        ),
      ),
    );
743
    tooltipKey.currentState?.ensureTooltipVisible();
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759
    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));
    expect(tooltipRenderParagraph.textSize.height, equals(10.0));

    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
    expect(tip.size.height, equals(24.0));
    expect(tip.size.width, equals(46.0));
    expect(tip, paints..rrect(
      rrect: RRect.fromRectAndRadius(tip.paintBounds, const Radius.circular(4.0)),
      color: const Color(0xe6616161),
    ));
  }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows}));

760
  testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async {
761
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
762 763 764 765 766 767 768 769 770 771 772 773
    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(
774
                  key: tooltipKey,
775 776
                  decoration: customDecoration,
                  message: tooltipText,
777
                  child: const SizedBox(
778 779 780 781 782 783 784 785 786 787
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
788
    tooltipKey.currentState?.ensureTooltipVisible();
789 790
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

791 792 793
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
794 795 796 797 798
    expect(tip.size.height, equals(32.0));
    expect(tip.size.width, equals(74.0));
    expect(tip, paints..path(
      color: const Color(0x80800000),
    ));
799
  });
800

801
  testWidgets('Tooltip stays after long press', (WidgetTester tester) async {
802
    await tester.pumpWidget(
803 804 805
      MaterialApp(
        home: Center(
          child: Tooltip(
806
            message: tooltipText,
807
            child: Container(
808 809
              width: 100.0,
              height: 100.0,
810
              color: Colors.green[500],
811 812 813
            ),
          ),
        ),
814
      ),
815 816
    );

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

    // long press reveals tooltip
821 822
    await tester.pump(kLongPressTimeout);
    await tester.pump(const Duration(milliseconds: 10));
823
    expect(find.text(tooltipText), findsOneWidget);
824 825 826 827
    await gesture.up();

    // tap (down, up) gesture hides tooltip, since its not
    // a long press
828 829
    await tester.tap(tooltip);
    await tester.pump(const Duration(milliseconds: 10));
830 831 832
    expect(find.text(tooltipText), findsNothing);

    // long press once more
833 834 835
    gesture = await tester.startGesture(tester.getCenter(tooltip));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 300));
836
    expect(find.text(tooltipText), findsNothing);
837

838
    await tester.pump(kLongPressTimeout);
839
    await tester.pump(const Duration(milliseconds: 10));
840
    expect(find.text(tooltipText), findsOneWidget);
841 842

    // keep holding the long press, should still show tooltip
843
    await tester.pump(kLongPressTimeout);
844
    expect(find.text(tooltipText), findsOneWidget);
845
    await gesture.up();
846 847
  });

848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
  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 {
      if (gesture != null)
        return gesture.removePointer();
    });
    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;
  });

904
  testWidgets('Tooltip shows/hides when hovered', (WidgetTester tester) async {
905
    const Duration waitDuration = Duration.zero;
906
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
907 908 909 910
    addTearDown(() async {
      if (gesture != null)
        return gesture.removePointer();
    });
Michael Goderbauer's avatar
Michael Goderbauer committed
911
    await gesture.addPointer();
912 913 914 915 916
    await gesture.moveTo(const Offset(1.0, 1.0));
    await tester.pump();
    await gesture.moveTo(Offset.zero);

    await tester.pumpWidget(
917
      const MaterialApp(
918 919 920 921
        home: Center(
          child: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
922
            child: SizedBox(
923 924 925 926 927
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
928
      ),
929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
    );

    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();
952
    gesture = null;
953 954 955
    expect(find.text(tooltipText), findsNothing);
  });

956 957 958 959
  testWidgets('Tooltip text is also hoverable', (WidgetTester tester) async {
    const Duration waitDuration = Duration.zero;
    TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
960
      gesture?.removePointer();
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 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009
    });
    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);
  });

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 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
  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);
  });

1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 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 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183
  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 {
      if (gesture != null)
        return gesture.removePointer();
    });
    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 {
      if (gesture != null)
        return gesture.removePointer();
    });
    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'),
              )
            ],
          ),
        ),
      ),
    );

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

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 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 {
      if (gesture != null)
        return gesture.removePointer();
    });
    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);
  });

1231
  testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
1232
    final SemanticsTester semantics = SemanticsTester(tester);
1233
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
1234

1235
    await tester.pumpWidget(
1236
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
1237
        textDirection: TextDirection.ltr,
1238
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
1239
          initialEntries: <OverlayEntry>[
1240
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
1241
              builder: (BuildContext context) {
1242
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
1243
                  children: <Widget>[
1244
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
1245 1246
                      left: 780.0,
                      top: 300.0,
1247
                      child: Tooltip(
1248
                        key: tooltipKey,
Ian Hickson's avatar
Ian Hickson committed
1249
                        message: tooltipText,
1250
                        child: const SizedBox(width: 10.0, height: 10.0),
Ian Hickson's avatar
Ian Hickson committed
1251 1252 1253 1254 1255 1256 1257 1258 1259
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
1260
    );
1261

1262
    final TestSemantics expected = TestSemantics.root(
1263
      children: <TestSemantics>[
1264
        TestSemantics.rootChild(
1265 1266 1267 1268
          id: 1,
          label: 'TIP',
          textDirection: TextDirection.ltr,
        ),
1269
      ],
1270 1271 1272
    );

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

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

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

1279
    expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
1280 1281

    semantics.dispose();
Hixie's avatar
Hixie committed
1282
  });
1283 1284 1285

  testWidgets('Tooltip overlay does not update', (WidgetTester tester) async {
    Widget buildApp(String text) {
1286 1287 1288
      return MaterialApp(
        home: Center(
          child: Tooltip(
1289
            message: text,
1290
            child: Container(
1291 1292
              width: 100.0,
              height: 100.0,
1293
              color: Colors.green[500],
1294 1295 1296
            ),
          ),
        ),
1297 1298 1299 1300 1301 1302 1303 1304
      );
    }

    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);
1305
    await tester.tapAt(const Offset(5.0, 5.0));
1306 1307 1308 1309 1310 1311 1312
    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);
  });

1313
  testWidgets('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
1314
    Widget buildApp(String text, { required double textScaleFactor }) {
1315 1316 1317
      return MediaQuery(
        data: MediaQueryData(textScaleFactor: textScaleFactor),
        child: Directionality(
1318
          textDirection: TextDirection.ltr,
1319
          child: Navigator(
1320
            onGenerateRoute: (RouteSettings settings) {
1321
              return MaterialPageRoute<void>(
1322
                builder: (BuildContext context) {
1323 1324
                  return Center(
                    child: Tooltip(
1325
                      message: text,
1326
                      child: Container(
1327 1328 1329 1330 1331 1332
                        width: 100.0,
                        height: 100.0,
                        color: Colors.green[500],
                      ),
                    ),
                  );
1333
                },
1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344
              );
            },
          ),
        ),
      );
    }

    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)));
1345 1346 1347
    RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
1348 1349 1350 1351 1352 1353
    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)));
1354 1355 1356
    tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
1357
    expect(tip.size.height, equals(56.0));
1358
  });
1359

1360
  testWidgets('Tooltip text displays with richMessage', (WidgetTester tester) async {
1361
    final GlobalKey<TooltipState> tooltipKey = GlobalKey<TooltipState>();
1362 1363 1364 1365 1366
    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(
1367
          key: tooltipKey,
1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383
          richMessage: const TextSpan(
            text: textSpan1Text,
            children: <InlineSpan>[
              TextSpan(
                text: textSpan2Text,
              ),
            ],
          ),
          child: Container(
            width: 100.0,
            height: 100.0,
            color: Colors.green[500],
          ),
        ),
      ),
    );
1384
    tooltipKey.currentState?.ensureTooltipVisible();
1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416
    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>()),
    );
  });

1417
  testWidgets('Haptic feedback', (WidgetTester tester) async {
1418
    final FeedbackTester feedback = FeedbackTester();
Ian Hickson's avatar
Ian Hickson committed
1419
    await tester.pumpWidget(
1420 1421 1422
      MaterialApp(
        home: Center(
          child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
1423
            message: 'Foo',
1424
            child: Container(
Ian Hickson's avatar
Ian Hickson committed
1425 1426 1427 1428 1429 1430 1431
              width: 100.0,
              height: 100.0,
              color: Colors.green[500],
            ),
          ),
        ),
      ),
1432 1433 1434 1435 1436 1437 1438 1439 1440
    );

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

    feedback.dispose();
  });

1441
  testWidgets('Semantics included', (WidgetTester tester) async {
1442
    final SemanticsTester semantics = SemanticsTester(tester);
1443 1444

    await tester.pumpWidget(
1445 1446
      const MaterialApp(
        home: Center(
1447
          child: Tooltip(
1448
            message: 'Foo',
1449
            child: Text('Bar'),
1450 1451 1452 1453 1454
          ),
        ),
      ),
    );

1455
    expect(semantics, hasSemantics(TestSemantics.root(
1456
      children: <TestSemantics>[
1457
        TestSemantics.rootChild(
1458
          children: <TestSemantics>[
1459
            TestSemantics(
1460
              children: <TestSemantics>[
1461
                TestSemantics(
1462 1463 1464 1465 1466 1467 1468
                  flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                  children: <TestSemantics>[
                    TestSemantics(
                      label: 'Foo\nBar',
                      textDirection: TextDirection.ltr,
                    ),
                  ],
1469
                ),
1470 1471
              ],
            ),
1472
          ],
1473 1474 1475 1476 1477 1478 1479 1480
        ),
      ],
    ), ignoreRect: true, ignoreId: true, ignoreTransform: true));

    semantics.dispose();
  });

  testWidgets('Semantics excluded', (WidgetTester tester) async {
1481
    final SemanticsTester semantics = SemanticsTester(tester);
1482 1483

    await tester.pumpWidget(
1484 1485
      const MaterialApp(
        home: Center(
1486
          child: Tooltip(
1487 1488
            message: 'Foo',
            excludeFromSemantics: true,
1489
            child: Text('Bar'),
1490 1491 1492 1493 1494
          ),
        ),
      ),
    );

1495
    expect(semantics, hasSemantics(TestSemantics.root(
1496
      children: <TestSemantics>[
1497
        TestSemantics.rootChild(
1498
          children: <TestSemantics>[
1499
            TestSemantics(
1500
              children: <TestSemantics>[
1501
                TestSemantics(
1502 1503 1504 1505 1506 1507 1508
                  flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                  children: <TestSemantics>[
                    TestSemantics(
                      label: 'Bar',
                      textDirection: TextDirection.ltr,
                    ),
                  ],
1509
                ),
1510 1511
              ],
            ),
1512
          ],
1513 1514 1515 1516 1517 1518 1519
        ),
      ],
    ), ignoreRect: true, ignoreId: true, ignoreTransform: true));

    semantics.dispose();
  });

1520 1521
  testWidgets('has semantic events', (WidgetTester tester) async {
    final List<dynamic> semanticEvents = <dynamic>[];
1522
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async {
1523 1524
      semanticEvents.add(message);
    });
1525
    final SemanticsTester semantics = SemanticsTester(tester);
1526 1527

    await tester.pumpWidget(
1528 1529 1530
      MaterialApp(
        home: Center(
          child: Tooltip(
1531
            message: 'Foo',
1532
            child: Container(
1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558
              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();
1559
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
1560
  });
1561 1562 1563
  testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

1564
    const Tooltip(message: 'message').debugFillProperties(builder);
1565 1566 1567 1568 1569 1570 1571 1572 1573

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

    expect(description, <String>[
      '"message"',
    ]);
  });
1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595
  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"',
    ]);
  });
1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606
  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,
1607
      margin: EdgeInsets.all(5.0),
1608 1609 1610 1611
      height: 100.0,
      excludeFromSemantics: true,
      preferBelow: false,
      verticalOffset: 50.0,
1612 1613
      triggerMode: TooltipTriggerMode.manual,
      enableFeedback: true,
1614 1615 1616 1617 1618 1619 1620 1621 1622 1623
    ).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',
1624
      'margin: EdgeInsets.all(5.0)',
1625 1626 1627 1628 1629
      'vertical offset: 50.0',
      'position: above',
      'semantics: excluded',
      'wait duration: 0:00:01.000000',
      'show duration: 0:00:02.000000',
1630 1631
      'triggerMode: TooltipTriggerMode.manual',
      'enableFeedback: true',
1632 1633
    ]);
  });
1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679

  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);
  });
1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705

  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);
    }
  });
1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730
}

Future<void> setWidgetForTooltipMode(WidgetTester tester, TooltipTriggerMode triggerMode) async {
  await tester.pumpWidget(
    MaterialApp(
      home: Tooltip(
        message: tooltipText,
        triggerMode: triggerMode,
        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));
1731 1732 1733 1734
}

SemanticsNode findDebugSemantics(RenderObject object) {
  if (object.debugSemantics != null)
1735 1736
    return object.debugSemantics!;
  return findDebugSemantics(object.parent! as RenderObject);
Hixie's avatar
Hixie committed
1737
}