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

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

Ian Hickson's avatar
Ian Hickson committed
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
// This file uses "as dynamic" in a few places to defeat the static
// analysis. In general you want to avoid using this style in your
// code, as it will cause the analyzer to be unable to help you catch
// errors.
//
// In this case, we do it because we are trying to call internal
// methods of the tooltip code in order to test it. Normally, the
// state of a tooltip is a private class, but by using a GlobalKey we
// can get a handle to that object and by using "as dynamic" we can
// bypass the analyzer's type checks and call methods that we aren't
// supposed to be able to know about.
//
// It's ok to do this in tests, but you really don't want to do it in
// production code.

33 34
const String tooltipText = 'TIP';

35 36 37 38 39 40 41
Finder _findTooltipContainer(String tooltipText) {
  return find.ancestor(
    of: find.text(tooltipText),
    matching: find.byType(Container),
  );
}

Hixie's avatar
Hixie committed
42
void main() {
43
  testWidgets('Does tooltip end up in the right place - center', (WidgetTester tester) async {
44
    final GlobalKey key = GlobalKey();
45
    await tester.pumpWidget(
46
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
47
        textDirection: TextDirection.ltr,
48
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
49
          initialEntries: <OverlayEntry>[
50
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
51
              builder: (BuildContext context) {
52
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
53
                  children: <Widget>[
54
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
55 56
                      left: 300.0,
                      top: 0.0,
57
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
58 59 60 61 62 63
                        key: key,
                        message: tooltipText,
                        height: 20.0,
                        padding: const EdgeInsets.all(5.0),
                        verticalOffset: 20.0,
                        preferBelow: false,
64
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
65 66 67 68 69 70 71 72 73 74 75 76
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
77
    );
78
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
79
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
80

81 82 83 84 85 86 87 88
    /********************* 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)
     *                   *
     *********************/
89

90 91 92
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
93
    final Offset tipInGlobal = tip.localToGlobal(tip.size.topCenter(Offset.zero));
94 95
    // The exact position of the left side depends on the font the test framework
    // happens to pick, so we don't test that.
96 97
    expect(tipInGlobal.dx, 300.0);
    expect(tipInGlobal.dy, 20.0);
98 99
  });

100
  testWidgets('Does tooltip end up in the right place - top left', (WidgetTester tester) async {
101
    final GlobalKey key = GlobalKey();
102
    await tester.pumpWidget(
103
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
104
        textDirection: TextDirection.ltr,
105
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
106
          initialEntries: <OverlayEntry>[
107
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
108
              builder: (BuildContext context) {
109
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
110
                  children: <Widget>[
111
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
112 113
                      left: 0.0,
                      top: 0.0,
114
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
115 116 117 118 119 120
                        key: key,
                        message: tooltipText,
                        height: 20.0,
                        padding: const EdgeInsets.all(5.0),
                        verticalOffset: 20.0,
                        preferBelow: false,
121
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
122 123 124 125 126 127 128 129 130 131 132 133
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
134
    );
135
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
136
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
137

138 139 140 141 142 143 144 145
    /********************* 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
146

147 148 149
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
150
    expect(tip.size.height, equals(24.0)); // 14.0 height + 5.0 padding * 2 (top, bottom)
151
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)), equals(const Offset(10.0, 20.0)));
152
  }, skip: isBrowser);
Hixie's avatar
Hixie committed
153

154
  testWidgets('Does tooltip end up in the right place - center prefer above fits', (WidgetTester tester) async {
155
    final GlobalKey key = GlobalKey();
156
    await tester.pumpWidget(
157
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
158
        textDirection: TextDirection.ltr,
159
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
160
          initialEntries: <OverlayEntry>[
161
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
162
              builder: (BuildContext context) {
163
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
164
                  children: <Widget>[
165
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
166 167
                      left: 400.0,
                      top: 300.0,
168
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
169 170 171 172 173 174
                        key: key,
                        message: tooltipText,
                        height: 100.0,
                        padding: const EdgeInsets.all(0.0),
                        verticalOffset: 100.0,
                        preferBelow: false,
175
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
176 177 178 179 180 181 182 183 184 185 186 187
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
188
    );
189
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
190
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
191

192
    /********************* 800x600 screen
193
     *        ___        * }- 10.0 margin
194 195 196 197 198 199 200
     *       |___|       * }-100.0 height
     *         |         * }-100.0 vertical offset
     *         o         * y=300.0
     *                   *
     *                   *
     *                   *
     *********************/
Hixie's avatar
Hixie committed
201

202 203 204
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
205
    expect(tip.size.height, equals(100.0));
206 207
    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
208 209
  });

210
  testWidgets('Does tooltip end up in the right place - center prefer above does not fit', (WidgetTester tester) async {
211
    final GlobalKey key = GlobalKey();
212
    await tester.pumpWidget(
213
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
214
        textDirection: TextDirection.ltr,
215
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
216
          initialEntries: <OverlayEntry>[
217
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
218
              builder: (BuildContext context) {
219
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
220
                  children: <Widget>[
221
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
222 223
                      left: 400.0,
                      top: 299.0,
224
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
225 226 227 228 229 230
                        key: key,
                        message: tooltipText,
                        height: 190.0,
                        padding: const EdgeInsets.all(0.0),
                        verticalOffset: 100.0,
                        preferBelow: false,
231
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
232 233 234 235 236 237 238 239 240 241 242 243
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
244
    );
245
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
246
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
247

248 249
    // we try to put it here but it doesn't fit:
    /********************* 800x600 screen
250 251
     *        ___        * }- 10.0 margin
     *       |___|       * }-190.0 height (starts at y=9.0)
252 253 254 255 256 257
     *         |         * }-100.0 vertical offset
     *         o         * y=299.0
     *                   *
     *                   *
     *                   *
     *********************/
Hixie's avatar
Hixie committed
258

259 260 261 262 263 264
    // so we put it here:
    /********************* 800x600 screen
     *                   *
     *                   *
     *         o         * y=299.0
     *        _|_        * }-100.0 vertical offset
265 266
     *       |___|       * }-190.0 height
     *                   * }- 10.0 margin
267
     *********************/
Hixie's avatar
Hixie committed
268

269 270 271
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
272
    expect(tip.size.height, equals(190.0));
273 274
    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
275 276
  });

277
  testWidgets('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async {
278
    final GlobalKey key = GlobalKey();
279
    await tester.pumpWidget(
280
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
281
        textDirection: TextDirection.ltr,
282
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
283
          initialEntries: <OverlayEntry>[
284
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
285
              builder: (BuildContext context) {
286
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
287
                  children: <Widget>[
288
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
289 290
                      left: 400.0,
                      top: 300.0,
291
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
292 293 294 295 296 297
                        key: key,
                        message: tooltipText,
                        height: 190.0,
                        padding: const EdgeInsets.all(0.0),
                        verticalOffset: 100.0,
                        preferBelow: true,
298
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
299 300 301 302 303 304 305 306 307 308 309 310
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
311
    );
312
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
313
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
314

315 316 317 318 319
    /********************* 800x600 screen
     *                   *
     *                   *
     *         o         * y=300.0
     *        _|_        * }-100.0 vertical offset
320 321
     *       |___|       * }-190.0 height
     *                   * }- 10.0 margin
322
     *********************/
Hixie's avatar
Hixie committed
323

324 325 326
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
327
    expect(tip.size.height, equals(190.0));
328 329
    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
330 331
  });

332
  testWidgets('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async {
333
    final GlobalKey key = GlobalKey();
334
    await tester.pumpWidget(
335
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
336
        textDirection: TextDirection.ltr,
337
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
338
          initialEntries: <OverlayEntry>[
339
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
340
              builder: (BuildContext context) {
341
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
342
                  children: <Widget>[
343
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
344 345
                      left: 1600.0,
                      top: 300.0,
346
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
347 348 349 350 351 352
                        key: key,
                        message: tooltipText,
                        height: 10.0,
                        padding: const EdgeInsets.all(0.0),
                        verticalOffset: 10.0,
                        preferBelow: true,
353
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
354 355 356 357 358 359 360 361 362 363 364 365
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
366
    );
367
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
368
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
369

370 371 372 373 374 375 376 377 378
    /********************* 800x600 screen
     *                   *
     *                   *
     *                   * y=300.0;   target -->   o
     *              ___| * }-10.0 vertical offset
     *             |___| * }-10.0 height
     *                   *
     *                   * }-10.0 margin
     *********************/
Hixie's avatar
Hixie committed
379

380 381 382
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
383
    expect(tip.size.height, equals(14.0));
384 385
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0));
386
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0));
387
  }, skip: isBrowser);
Hixie's avatar
Hixie committed
388

389
  testWidgets('Does tooltip end up in the right place - near the edge', (WidgetTester tester) async {
390
    final GlobalKey key = GlobalKey();
391
    await tester.pumpWidget(
392
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
393
        textDirection: TextDirection.ltr,
394
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
395
          initialEntries: <OverlayEntry>[
396
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
397
              builder: (BuildContext context) {
398
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
399
                  children: <Widget>[
400
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
401 402
                      left: 780.0,
                      top: 300.0,
403
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
404 405 406 407 408 409
                        key: key,
                        message: tooltipText,
                        height: 10.0,
                        padding: const EdgeInsets.all(0.0),
                        verticalOffset: 10.0,
                        preferBelow: true,
410
                        child: Container(
Ian Hickson's avatar
Ian Hickson committed
411 412 413 414 415 416 417 418 419 420 421 422
                          width: 0.0,
                          height: 0.0,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
423
    );
424
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
425
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
Hixie's avatar
Hixie committed
426

427 428 429 430 431 432 433 434 435
    /********************* 800x600 screen
     *                   *
     *                   *
     *                o  * y=300.0
     *              __|  * }-10.0 vertical offset
     *             |___| * }-10.0 height
     *                   *
     *                   * }-10.0 margin
     *********************/
Hixie's avatar
Hixie committed
436

437 438 439
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
440
    expect(tip.size.height, equals(14.0));
441 442
    expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0));
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0));
443
    expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0));
444
  }, skip: isBrowser);
Hixie's avatar
Hixie committed
445

446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
  testWidgets('Custom tooltip margin', (WidgetTester tester) async {
    const double _customMarginValue = 10.0;
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
                  key: key,
                  message: tooltipText,
                  padding: const EdgeInsets.all(0.0),
                  margin: const EdgeInsets.all(_customMarginValue),
                  child: Container(
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    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));
    expect(topLeftTooltipContentInGlobal.dx, topLeftTipInGlobal.dx + _customMarginValue);
    expect(topLeftTooltipContentInGlobal.dy, topLeftTipInGlobal.dy + _customMarginValue);

    final Offset topRightTipInGlobal = tester.getTopRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset topRightTooltipContentInGlobal = tester.getTopRight(find.text(tooltipText));
    expect(topRightTooltipContentInGlobal.dx, topRightTipInGlobal.dx - _customMarginValue);
    expect(topRightTooltipContentInGlobal.dy, topRightTipInGlobal.dy + _customMarginValue);

    final Offset bottomLeftTipInGlobal = tester.getBottomLeft(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomLeftTooltipContentInGlobal = tester.getBottomLeft(find.text(tooltipText));
    expect(bottomLeftTooltipContentInGlobal.dx, bottomLeftTipInGlobal.dx + _customMarginValue);
    expect(bottomLeftTooltipContentInGlobal.dy, bottomLeftTipInGlobal.dy - _customMarginValue);

    final Offset bottomRightTipInGlobal = tester.getBottomRight(
      _findTooltipContainer(tooltipText),
    );
    final Offset bottomRightTooltipContentInGlobal = tester.getBottomRight(find.text(tooltipText));
    expect(bottomRightTooltipContentInGlobal.dx, bottomRightTipInGlobal.dx - _customMarginValue);
    expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - _customMarginValue);
  });

504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
  testWidgets('Default tooltip message textStyle - light', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
        key: key,
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style;
    expect(textStyle.color, Colors.white);
    expect(textStyle.fontFamily, 'Roboto');
    expect(textStyle.decoration, TextDecoration.none);
524
    expect(textStyle.debugLabel, '((englishLike body1 2014).merge(blackMountainView bodyText2)).copyWith');
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
  });

  testWidgets('Default tooltip message textStyle - dark', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(
        brightness: Brightness.dark,
      ),
      home: Tooltip(
        key: key,
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

    final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style;
    expect(textStyle.color, Colors.black);
    expect(textStyle.fontFamily, 'Roboto');
    expect(textStyle.decoration, TextDecoration.none);
550
    expect(textStyle.debugLabel, '((englishLike body1 2014).merge(whiteMountainView bodyText2)).copyWith');
551 552 553 554 555 556 557 558 559
  });

  testWidgets('Custom tooltip message textStyle', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
        key: key,
        textStyle: const TextStyle(
          color: Colors.orange,
560
          decoration: TextDecoration.underline,
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
        ),
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

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

579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
  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);
  });

612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
  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.
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(MaterialApp(
      home: Tooltip(
        key: key,
        message: tooltipText,
        child: Container(
          width: 100.0,
          height: 100.0,
          color: Colors.green[500],
        ),
      ),
    ));
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    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));
  });

645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
  testWidgets('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Overlay(
          initialEntries: <OverlayEntry>[
            OverlayEntry(
              builder: (BuildContext context) {
                return Tooltip(
                  key: key,
                  message: tooltipText,
                  child: Container(
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

671 672 673
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
674 675 676 677 678 679
    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),
    ));
680
  }, skip: isBrowser);
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712

  testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    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(
                  key: key,
                  decoration: customDecoration,
                  message: tooltipText,
                  child: Container(
                    width: 0.0,
                    height: 0.0,
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
    (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
    await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)

713 714 715
    final RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
716 717 718 719 720
    expect(tip.size.height, equals(32.0));
    expect(tip.size.width, equals(74.0));
    expect(tip, paints..path(
      color: const Color(0x80800000),
    ));
721
  }, skip: isBrowser);
722

723
  testWidgets('Tooltip stays after long press', (WidgetTester tester) async {
724
    await tester.pumpWidget(
725 726 727
      MaterialApp(
        home: Center(
          child: Tooltip(
728
            message: tooltipText,
729
            child: Container(
730 731
              width: 100.0,
              height: 100.0,
732
              color: Colors.green[500],
733 734 735
            ),
          ),
        ),
736
      ),
737 738
    );

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

    // long press reveals tooltip
743 744
    await tester.pump(kLongPressTimeout);
    await tester.pump(const Duration(milliseconds: 10));
745
    expect(find.text(tooltipText), findsOneWidget);
746 747 748 749
    await gesture.up();

    // tap (down, up) gesture hides tooltip, since its not
    // a long press
750 751
    await tester.tap(tooltip);
    await tester.pump(const Duration(milliseconds: 10));
752 753 754
    expect(find.text(tooltipText), findsNothing);

    // long press once more
755 756 757
    gesture = await tester.startGesture(tester.getCenter(tooltip));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 300));
758
    expect(find.text(tooltipText), findsNothing);
759

760
    await tester.pump(kLongPressTimeout);
761
    await tester.pump(const Duration(milliseconds: 10));
762
    expect(find.text(tooltipText), findsOneWidget);
763 764

    // keep holding the long press, should still show tooltip
765
    await tester.pump(kLongPressTimeout);
766
    expect(find.text(tooltipText), findsOneWidget);
767 768 769
    gesture.up();
  });

770 771
  testWidgets('Tooltip shows/hides when hovered', (WidgetTester tester) async {
    const Duration waitDuration = Duration(milliseconds: 0);
772 773 774 775 776
    TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(() async {
      if (gesture != null)
        return gesture.removePointer();
    });
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
    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: Tooltip(
            message: tooltipText,
            waitDuration: waitDuration,
            child: Container(
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
794
      ),
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
    );

    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();
818
    gesture = null;
819 820 821
    expect(find.text(tooltipText), findsNothing);
  });

822
  testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
823
    final SemanticsTester semantics = SemanticsTester(tester);
824

825
    final GlobalKey key = GlobalKey();
826
    await tester.pumpWidget(
827
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
828
        textDirection: TextDirection.ltr,
829
        child: Overlay(
Ian Hickson's avatar
Ian Hickson committed
830
          initialEntries: <OverlayEntry>[
831
            OverlayEntry(
Ian Hickson's avatar
Ian Hickson committed
832
              builder: (BuildContext context) {
833
                return Stack(
Ian Hickson's avatar
Ian Hickson committed
834
                  children: <Widget>[
835
                    Positioned(
Ian Hickson's avatar
Ian Hickson committed
836 837
                      left: 780.0,
                      top: 300.0,
838
                      child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
839 840
                        key: key,
                        message: tooltipText,
841
                        child: Container(width: 10.0, height: 10.0),
Ian Hickson's avatar
Ian Hickson committed
842 843 844 845 846 847 848 849 850
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
851
    );
852

853
    final TestSemantics expected = TestSemantics.root(
854
      children: <TestSemantics>[
855
        TestSemantics.rootChild(
856 857 858 859
          id: 1,
          label: 'TIP',
          textDirection: TextDirection.ltr,
        ),
860
      ],
861 862 863
    );

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

865
    // Before using "as dynamic" in your code, see note at the top of the file.
866
    (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes
Hixie's avatar
Hixie committed
867

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

870
    expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
871 872

    semantics.dispose();
Hixie's avatar
Hixie committed
873
  });
874 875 876

  testWidgets('Tooltip overlay does not update', (WidgetTester tester) async {
    Widget buildApp(String text) {
877 878 879
      return MaterialApp(
        home: Center(
          child: Tooltip(
880
            message: text,
881
            child: Container(
882 883
              width: 100.0,
              height: 100.0,
884
              color: Colors.green[500],
885 886 887
            ),
          ),
        ),
888 889 890 891 892 893 894 895
      );
    }

    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);
896
    await tester.tapAt(const Offset(5.0, 5.0));
897 898 899 900 901 902 903
    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);
  });

904 905
  testWidgets('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
    Widget buildApp(String text, { double textScaleFactor }) {
906 907 908
      return MediaQuery(
        data: MediaQueryData(textScaleFactor: textScaleFactor),
        child: Directionality(
909
          textDirection: TextDirection.ltr,
910
          child: Navigator(
911
            onGenerateRoute: (RouteSettings settings) {
912
              return MaterialPageRoute<void>(
913
                builder: (BuildContext context) {
914 915
                  return Center(
                    child: Tooltip(
916
                      message: text,
917
                      child: Container(
918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
                        width: 100.0,
                        height: 100.0,
                        color: Colors.green[500],
                      ),
                    ),
                  );
                }
              );
            },
          ),
        ),
      );
    }

    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)));
936 937 938
    RenderBox tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
939 940 941 942 943 944
    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)));
945 946 947
    tip = tester.renderObject(
      _findTooltipContainer(tooltipText),
    );
948
    expect(tip.size.height, equals(56.0));
949
  }, skip: isBrowser);
950

951
  testWidgets('Haptic feedback', (WidgetTester tester) async {
952
    final FeedbackTester feedback = FeedbackTester();
Ian Hickson's avatar
Ian Hickson committed
953
    await tester.pumpWidget(
954 955 956
      MaterialApp(
        home: Center(
          child: Tooltip(
Ian Hickson's avatar
Ian Hickson committed
957
            message: 'Foo',
958
            child: Container(
Ian Hickson's avatar
Ian Hickson committed
959 960 961 962 963 964 965
              width: 100.0,
              height: 100.0,
              color: Colors.green[500],
            ),
          ),
        ),
      ),
966 967 968 969 970 971 972 973 974
    );

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

    feedback.dispose();
  });

975
  testWidgets('Semantics included', (WidgetTester tester) async {
976
    final SemanticsTester semantics = SemanticsTester(tester);
977 978

    await tester.pumpWidget(
979 980
      const MaterialApp(
        home: Center(
981
          child: Tooltip(
982
            message: 'Foo',
983
            child: Text('Bar'),
984 985 986 987 988
          ),
        ),
      ),
    );

989
    expect(semantics, hasSemantics(TestSemantics.root(
990
      children: <TestSemantics>[
991
        TestSemantics.rootChild(
992
          children: <TestSemantics>[
993
            TestSemantics(
994 995
              flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
              children: <TestSemantics>[
996
                TestSemantics(
997 998
                  label: 'Foo\nBar',
                  textDirection: TextDirection.ltr,
999
                ),
1000 1001
              ],
            ),
1002
          ],
1003 1004 1005 1006 1007 1008 1009 1010
        ),
      ],
    ), ignoreRect: true, ignoreId: true, ignoreTransform: true));

    semantics.dispose();
  });

  testWidgets('Semantics excluded', (WidgetTester tester) async {
1011
    final SemanticsTester semantics = SemanticsTester(tester);
1012 1013

    await tester.pumpWidget(
1014 1015
      const MaterialApp(
        home: Center(
1016
          child: Tooltip(
1017
            message: 'Foo',
1018
            child: Text('Bar'),
1019 1020 1021 1022 1023 1024
            excludeFromSemantics: true,
          ),
        ),
      ),
    );

1025
    expect(semantics, hasSemantics(TestSemantics.root(
1026
      children: <TestSemantics>[
1027
        TestSemantics.rootChild(
1028
          children: <TestSemantics>[
1029
            TestSemantics(
1030 1031
              flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
              children: <TestSemantics>[
1032
                TestSemantics(
1033 1034
                  label: 'Bar',
                  textDirection: TextDirection.ltr,
1035
                ),
1036 1037
              ],
            ),
1038
          ],
1039 1040 1041 1042 1043 1044 1045
        ),
      ],
    ), ignoreRect: true, ignoreId: true, ignoreTransform: true));

    semantics.dispose();
  });

1046 1047
  testWidgets('has semantic events', (WidgetTester tester) async {
    final List<dynamic> semanticEvents = <dynamic>[];
1048
    SystemChannels.accessibility.setMockMessageHandler((dynamic message) async {
1049 1050
      semanticEvents.add(message);
    });
1051
    final SemanticsTester semantics = SemanticsTester(tester);
1052 1053

    await tester.pumpWidget(
1054 1055 1056
      MaterialApp(
        home: Center(
          child: Tooltip(
1057
            message: 'Foo',
1058
            child: Container(
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
              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();
    SystemChannels.accessibility.setMockMessageHandler(null);
  });
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110
  testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

    const Tooltip(message: 'message',).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"',
    ]);
  });
  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,
1111
      margin: EdgeInsets.all(5.0),
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
      height: 100.0,
      excludeFromSemantics: true,
      preferBelow: false,
      verticalOffset: 50.0,
    ).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',
1126
      'margin: EdgeInsets.all(5.0)',
1127 1128 1129 1130 1131 1132 1133
      'vertical offset: 50.0',
      'position: above',
      'semantics: excluded',
      'wait duration: 0:00:01.000000',
      'show duration: 0:00:02.000000',
    ]);
  });
1134 1135 1136 1137 1138
}

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