text_test.dart 33 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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
// @dart = 2.8

7 8
import 'dart:ui' as ui;

9
import 'package:flutter_test/flutter_test.dart';
10
import 'package:flutter/foundation.dart';
11
import 'package:flutter/gestures.dart';
12
import 'package:flutter/material.dart';
13
import 'package:flutter/widgets.dart';
14

15
import '../rendering/mock_canvas.dart';
16 17
import 'semantics_tester.dart';

18 19
void main() {
  testWidgets('Text respects media query', (WidgetTester tester) async {
20
    await tester.pumpWidget(const MediaQuery(
21 22
      data: MediaQueryData(textScaleFactor: 1.3),
      child: Center(
23 24
        child: Text('Hello', textDirection: TextDirection.ltr),
      ),
25 26 27 28
    ));

    RichText text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
29
    expect(text.textScaleFactor, 1.3);
30

31
    await tester.pumpWidget(const Center(
32
      child: Text('Hello', textDirection: TextDirection.ltr),
33 34 35 36 37
    ));

    text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
    expect(text.textScaleFactor, 1.0);
38 39 40 41
  });

  testWidgets('Text respects textScaleFactor with default font size', (WidgetTester tester) async {
    await tester.pumpWidget(
42
      const Center(child: Text('Hello', textDirection: TextDirection.ltr))
43 44 45 46 47 48 49 50 51 52
    );

    RichText text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
    expect(text.textScaleFactor, 1.0);
    final Size baseSize = tester.getSize(find.byType(RichText));
    expect(baseSize.width, equals(70.0));
    expect(baseSize.height, equals(14.0));

    await tester.pumpWidget(const Center(
53 54 55 56 57
      child: Text(
        'Hello',
        textScaleFactor: 1.5,
        textDirection: TextDirection.ltr,
      ),
58 59 60 61 62 63 64 65
    ));

    text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
    expect(text.textScaleFactor, 1.5);
    final Size largeSize = tester.getSize(find.byType(RichText));
    expect(largeSize.width, 105.0);
    expect(largeSize.height, equals(21.0));
66
  });
67 68 69

  testWidgets('Text respects textScaleFactor with explicit font size', (WidgetTester tester) async {
    await tester.pumpWidget(const Center(
70
      child: Text('Hello',
71
        style: TextStyle(fontSize: 20.0), textDirection: TextDirection.ltr),
72 73 74 75 76 77 78 79
    ));

    RichText text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
    expect(text.textScaleFactor, 1.0);
    final Size baseSize = tester.getSize(find.byType(RichText));
    expect(baseSize.width, equals(100.0));
    expect(baseSize.height, equals(20.0));
80

81
    await tester.pumpWidget(const Center(
82 83
      child: Text('Hello',
        style: TextStyle(fontSize: 20.0),
84
        textScaleFactor: 1.3,
85
        textDirection: TextDirection.ltr),
86 87 88 89
    ));

    text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
90 91 92 93
    expect(text.textScaleFactor, 1.3);
    final Size largeSize = tester.getSize(find.byType(RichText));
    expect(largeSize.width, anyOf(131.0, 130.0));
    expect(largeSize.height, equals(26.0));
94
  });
95

96
  testWidgets("Text throws a nice error message if there's no Directionality", (WidgetTester tester) async {
97 98 99 100 101
    await tester.pumpWidget(const Text('Hello'));
    final String message = tester.takeException().toString();
    expect(message, contains('Directionality'));
    expect(message, contains(' Text '));
  });
102 103 104 105

  testWidgets('Text can be created from TextSpans and uses defaultTextStyle', (WidgetTester tester) async {
    await tester.pumpWidget(
      const DefaultTextStyle(
106
        style: TextStyle(
107 108
          fontSize: 20.0,
        ),
109 110
        child: Text.rich(
          TextSpan(
111
            text: 'Hello',
112
            children: <TextSpan>[
113 114 115 116 117 118 119 120
              TextSpan(
                text: ' beautiful ',
                style: TextStyle(fontStyle: FontStyle.italic),
              ),
              TextSpan(
                text: 'world',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
121 122 123 124 125 126 127 128 129 130 131
            ],
          ),
          textDirection: TextDirection.ltr,
        ),
      ),
    );

    final RichText text = tester.firstWidget(find.byType(RichText));
    expect(text, isNotNull);
    expect(text.text.style.fontSize, 20.0);
  });
132

133 134 135 136 137 138 139
  testWidgets('inline widgets works with ellipsis', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/35869
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
      Text.rich(
        TextSpan(
          children: <InlineSpan>[
140 141 142
            const TextSpan(
              text: 'a very very very very very very very very very very long line',
            ),
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
            WidgetSpan(
              child: SizedBox(
                width: 20,
                height: 40,
                child: Card(
                  child: RichText(
                    text: const TextSpan(text: 'widget should be truncated'),
                    textDirection: TextDirection.rtl,
                  ),
                ),
              ),
            ),
          ],
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
    );
    expect(tester.takeException(), null);
164
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42086
165

166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
  testWidgets('inline widgets works with textScaleFactor', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/59316
    final UniqueKey key = UniqueKey();
    double textScaleFactor = 1.0;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('title')),
          body: Center(
            child: Text.rich(
              TextSpan(
                children: <InlineSpan>[
                  WidgetSpan(
                    child: RichText(
                      text: const TextSpan(text: 'widget should be truncated'),
                      textDirection: TextDirection.ltr,
                    ),
                  ),
                ],
              ),
              key: key,
              textDirection: TextDirection.ltr,
              textScaleFactor: textScaleFactor,
            ),
          ),
        ),
      ),
    );
    RenderBox renderText = tester.renderObject(find.byKey(key));
    final double singleLineHeight = renderText.size.height;
    // Now, increases the text scale factor by 5 times.
    textScaleFactor = textScaleFactor * 5;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('title')),
          body: Center(
            child: Text.rich(
              TextSpan(
                children: <InlineSpan>[
                  WidgetSpan(
                    child: RichText(
                      text: const TextSpan(text: 'widget should be truncated'),
                      textDirection: TextDirection.ltr,
                    ),
                  ),
                ],
              ),
              key: key,
              textDirection: TextDirection.ltr,
              textScaleFactor: textScaleFactor,
            ),
          ),
        ),
      ),
    );

    renderText = tester.renderObject(find.byKey(key));
    // The RichText in the widget span should wrap into three lines.
    expect(renderText.size.height, singleLineHeight * textScaleFactor * 3);
226
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42086
227

228
  testWidgets('semanticsLabel can override text label', (WidgetTester tester) async {
229
    final SemanticsTester semantics = SemanticsTester(tester);
230
    await tester.pumpWidget(
231
      const Text(
232
        r'$$',
233 234 235
        semanticsLabel: 'Double dollars',
        textDirection: TextDirection.ltr,
      )
236
    );
237
    final TestSemantics expectedSemantics = TestSemantics.root(
238
      children: <TestSemantics>[
239
        TestSemantics.rootChild(
240 241 242 243 244
          label: 'Double dollars',
          textDirection: TextDirection.ltr,
        ),
      ],
    );
245 246 247 248 249 250 251 252 253
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
254 255 256 257

    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
258
        child: Text(r'$$', semanticsLabel: 'Double dollars')),
259 260
    );

261 262 263 264 265 266 267 268 269
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
270 271
    semantics.dispose();
  });
272

273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  testWidgets('semanticsLabel can be shorter than text', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: RichText(
        text: TextSpan(
        children: <InlineSpan>[
          const TextSpan(
            text: 'Some Text',
            semanticsLabel: '',
          ),
          TextSpan(
            text: 'Clickable',
            recognizer: TapGestureRecognizer()..onTap = () { },
          ),
        ]),
      ),
    ));
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics(
          children: <TestSemantics>[
            TestSemantics(
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'Clickable',
              actions: <SemanticsAction>[SemanticsAction.tap],
301
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
302 303 304 305 306 307
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    );
308 309 310 311 312 313 314 315 316
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
317 318 319
    semantics.dispose();
  });

320
  testWidgets('recognizers split semantic node', (WidgetTester tester) async {
321
    final SemanticsTester semantics = SemanticsTester(tester);
322 323
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
324 325
      Text.rich(
        TextSpan(
326 327
          children: <TextSpan>[
            const TextSpan(text: 'hello '),
328 329 330 331
            TextSpan(
              text: 'world',
              recognizer: TapGestureRecognizer()..onTap = () { },
            ),
332 333 334 335 336 337 338 339
            const TextSpan(text: ' this is a '),
            const TextSpan(text: 'cat-astrophe'),
          ],
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
      ),
    );
340
    final TestSemantics expectedSemantics = TestSemantics.root(
341
      children: <TestSemantics>[
342
        TestSemantics.rootChild(
343
          children: <TestSemantics>[
344
            TestSemantics(
345 346 347
              label: 'hello ',
              textDirection: TextDirection.ltr,
            ),
348
            TestSemantics(
349 350
              label: 'world',
              textDirection: TextDirection.ltr,
351 352
              actions: <SemanticsAction>[SemanticsAction.tap],
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
353
            ),
354
            TestSemantics(
355 356
              label: ' this is a cat-astrophe',
              textDirection: TextDirection.ltr,
357
            ),
358 359 360 361
          ],
        ),
      ],
    );
362 363 364 365 366 367 368 369 370
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
371 372 373
    semantics.dispose();
  });

374 375 376 377 378 379 380 381 382 383
  testWidgets('recognizers split semantic node when TextSpan overflows', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
      SizedBox(
        height: 10,
        child: Text.rich(
          TextSpan(
            children: <TextSpan>[
              const TextSpan(text: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'),
384 385 386 387
              TextSpan(
                text: 'world',
                recognizer: TapGestureRecognizer()..onTap = () { },
              ),
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
            ],
            style: textStyle,
          ),
          textDirection: TextDirection.ltr,
        ),
      ),
    );
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'world',
              textDirection: TextDirection.ltr,
406
              actions: <SemanticsAction>[SemanticsAction.tap],
407
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
408 409 410 411 412
            ),
          ],
        ),
      ],
    );
413 414 415 416 417 418 419 420 421
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
422 423 424
    semantics.dispose();
  });

425 426 427 428 429 430 431 432
  testWidgets('recognizers split semantic nodes with text span labels', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
      Text.rich(
        TextSpan(
          children: <TextSpan>[
            const TextSpan(text: 'hello '),
433 434 435 436
            TextSpan(
              text: 'world',
              recognizer: TapGestureRecognizer()..onTap = () { },
            ),
437
            const TextSpan(text: ' this is a '),
438 439 440 441
            const TextSpan(
              text: 'cat-astrophe',
              semanticsLabel: 'regrettable event',
            ),
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
          ],
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
      ),
    );
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: 'hello ',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'world',
              textDirection: TextDirection.ltr,
459 460
              actions: <SemanticsAction>[SemanticsAction.tap],
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
461 462
            ),
            TestSemantics(
463
              label: ' this is a regrettable event',
464 465 466 467 468 469
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    );
470 471 472 473 474 475 476 477 478
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
479
    semantics.dispose();
480
  });
481 482


483
  testWidgets('recognizers split semantic node - bidi', (WidgetTester tester) async {
484
    final SemanticsTester semantics = SemanticsTester(tester);
485 486
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
487 488
      RichText(
        text: TextSpan(
489 490 491
          style: textStyle,
          children: <TextSpan>[
            const TextSpan(text: 'hello world${Unicode.RLE}${Unicode.RLO} '),
492 493 494 495
            TextSpan(
              text: 'BOY',
              recognizer: LongPressGestureRecognizer()..onLongPress = () { },
            ),
496
            const TextSpan(text: ' HOW DO${Unicode.PDF} you ${Unicode.RLO} DO '),
497 498 499 500
            TextSpan(
              text: 'SIR',
              recognizer: TapGestureRecognizer()..onTap = () { },
            ),
501 502 503 504
            const TextSpan(text: '${Unicode.PDF}${Unicode.PDF} good bye'),
          ],
        ),
        textDirection: TextDirection.ltr,
505
      ),
506 507 508
    );
    // The expected visual order of the text is:
    //   hello world RIS OD you OD WOH YOB good bye
509 510 511 512 513 514 515
    // There are five unique text areas, they are, in visual order but
    // showing the logical text:
    //   [hello world][SIR][HOW DO you DO][BOY][good bye]
    // The direction of each varies based on the first bit of that area.
    // The presence of the bidi formatting characters in the text is a
    // bit dubious, but that's what we do currently, and it's not really
    // clear what the perfect behavior would be...
516
    final TestSemantics expectedSemantics = TestSemantics.root(
517
      children: <TestSemantics>[
518
        TestSemantics.rootChild(
Dan Field's avatar
Dan Field committed
519
          rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
520
          children: <TestSemantics>[
521
            TestSemantics(
Dan Field's avatar
Dan Field committed
522
              rect: const Rect.fromLTRB(-4.0, -4.0, 480.0, 18.0),
523 524
              label: 'hello world${Unicode.RLE}${Unicode.RLO} ',
              textDirection: TextDirection.ltr,
525
            ),
526
            TestSemantics(
527 528 529 530
              rect: const Rect.fromLTRB(416.0, -4.0, 466.0, 18.0),
              label: 'BOY',
              textDirection: TextDirection.rtl,
              actions: <SemanticsAction>[SemanticsAction.longPress],
531
            ),
532
            TestSemantics(
Dan Field's avatar
Dan Field committed
533
              rect: const Rect.fromLTRB(192.0, -4.0, 424.0, 18.0),
534
              label: ' HOW DO${Unicode.PDF} you ${Unicode.RLO} DO ',
535 536
              textDirection: TextDirection.rtl,
            ),
537
            TestSemantics(
538 539 540 541 542
              rect: const Rect.fromLTRB(150.0, -4.0, 200.0, 18.0),
              label: 'SIR',
              textDirection: TextDirection.rtl,
              actions: <SemanticsAction>[SemanticsAction.tap],
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
543
            ),
544
            TestSemantics(
Dan Field's avatar
Dan Field committed
545
              rect: const Rect.fromLTRB(472.0, -4.0, 606.0, 18.0),
546 547
              label: '${Unicode.PDF}${Unicode.PDF} good bye',
              textDirection: TextDirection.rtl,
548 549 550 551 552
            ),
          ],
        ),
      ],
    );
553 554 555 556 557 558 559 560
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
      ),
    );
561
    semantics.dispose();
562
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945
563

564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 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
  testWidgets('TapGesture recognizers contribute link semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
      Text.rich(
        TextSpan(
          children: <TextSpan>[
            TextSpan(
              text: 'click me',
              recognizer: TapGestureRecognizer()..onTap = () { },
            ),
          ],
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
      ),
    );
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: 'click me',
              textDirection: TextDirection.ltr,
              actions: <SemanticsAction>[SemanticsAction.tap],
              flags: <SemanticsFlag>[SemanticsFlag.isLink]
            ),
          ],
        ),
      ],
    );
    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreId: true,
      ignoreRect: true,
    ));
    semantics.dispose();
  });

604 605 606 607 608 609 610 611
  testWidgets('inline widgets generate semantic nodes', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
      Text.rich(
        TextSpan(
          children: <InlineSpan>[
            const TextSpan(text: 'a '),
612 613 614 615
            TextSpan(
              text: 'pebble',
              recognizer: TapGestureRecognizer()..onTap = () { },
            ),
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 645 646
            const TextSpan(text: ' in the '),
            WidgetSpan(
              child: SizedBox(
                width: 20,
                height: 40,
                child: Card(
                  child: RichText(
                    text: const TextSpan(text: 'INTERRUPTION'),
                    textDirection: TextDirection.rtl,
                  ),
                ),
              ),
            ),
            const TextSpan(text: 'sky'),
          ],
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
      ),
    );
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: 'a ',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'pebble',
              textDirection: TextDirection.ltr,
647 648
              actions: <SemanticsAction>[SemanticsAction.tap],
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
            ),
            TestSemantics(
              label: ' in the ',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'INTERRUPTION',
              textDirection: TextDirection.rtl,
            ),
            TestSemantics(
              label: 'sky',
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    );
666 667 668 669 670 671 672 673 674
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
        ignoreRect: true,
      ),
    );
675
    semantics.dispose();
676
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42086
677 678 679 680 681 682 683 684 685

  testWidgets('inline widgets semantic nodes scale', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
    await tester.pumpWidget(
      Text.rich(
        TextSpan(
          children: <InlineSpan>[
            const TextSpan(text: 'a '),
686 687 688 689
            TextSpan(
              text: 'pebble',
              recognizer: TapGestureRecognizer()..onTap = () { },
            ),
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
            const TextSpan(text: ' in the '),
            WidgetSpan(
              child: SizedBox(
                width: 20,
                height: 40,
                child: Card(
                  child: RichText(
                    text: const TextSpan(text: 'INTERRUPTION'),
                    textDirection: TextDirection.rtl,
                  ),
                ),
              ),
            ),
            const TextSpan(text: 'sky'),
          ],
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
        textScaleFactor: 2,
      ),
    );
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
          children: <TestSemantics>[
            TestSemantics(
              label: 'a ',
              textDirection: TextDirection.ltr,
              rect: const Rect.fromLTRB(-4.0, 48.0, 60.0, 84.0),
            ),
            TestSemantics(
              label: 'pebble',
              textDirection: TextDirection.ltr,
724 725
              actions: <SemanticsAction>[SemanticsAction.tap],
              flags: <SemanticsFlag>[SemanticsFlag.isLink],
726 727 728 729 730 731 732 733 734 735
              rect: const Rect.fromLTRB(52.0, 48.0, 228.0, 84.0),
            ),
            TestSemantics(
              label: ' in the ',
              textDirection: TextDirection.ltr,
              rect: const Rect.fromLTRB(220.0, 48.0, 452.0, 84.0),
            ),
            TestSemantics(
              label: 'INTERRUPTION',
              textDirection: TextDirection.rtl,
736
              rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 80.0),
737 738 739 740 741 742 743 744 745 746
            ),
            TestSemantics(
              label: 'sky',
              textDirection: TextDirection.ltr,
              rect: const Rect.fromLTRB(484.0, 48.0, 576.0, 84.0),
            ),
          ],
        ),
      ],
    );
747 748 749 750 751 752 753 754
    expect(
      semantics,
      hasSemantics(
        expectedSemantics,
        ignoreTransform: true,
        ignoreId: true,
      ),
    );
755
    semantics.dispose();
756
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42086
757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774

  testWidgets('Overflow is clipping correctly - short text with overflow: clip', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.clip,
      text: 'Hi',
    );

    expect(find.byType(Text), isNot(paints..clipRect()));
  });

  testWidgets('Overflow is clipping correctly - long text with overflow: ellipsis', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.ellipsis,
      text: 'a long long long long text, should be clip',
    );

775 776 777 778
    expect(
      find.byType(Text),
      paints..clipRect(rect: const Rect.fromLTWH(0, 0, 50, 50)),
    );
779
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33523
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797

  testWidgets('Overflow is clipping correctly - short text with overflow: ellipsis', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.ellipsis,
      text: 'Hi',
    );

    expect(find.byType(Text), isNot(paints..clipRect()));
  });

  testWidgets('Overflow is clipping correctly - long text with overflow: fade', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.fade,
      text: 'a long long long long text, should be clip',
    );

798 799 800 801
    expect(
      find.byType(Text),
      paints..clipRect(rect: const Rect.fromLTWH(0, 0, 50, 50)),
    );
802 803 804 805 806 807 808 809 810 811 812
  });

  testWidgets('Overflow is clipping correctly - short text with overflow: fade', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.fade,
      text: 'Hi',
    );

    expect(find.byType(Text), isNot(paints..clipRect()));
  });
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832

  testWidgets('Overflow is clipping correctly - long text with overflow: visible', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.visible,
      text: 'a long long long long text, should be clip',
    );

    expect(find.byType(Text), isNot(paints..clipRect()));
  });

  testWidgets('Overflow is clipping correctly - short text with overflow: visible', (WidgetTester tester) async {
    await _pumpTextWidget(
      tester: tester,
      overflow: TextOverflow.visible,
      text: 'Hi',
    );

    expect(find.byType(Text), isNot(paints..clipRect()));
  });
833 834 835 836 837 838 839 840 841 842

  testWidgets('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async {
    Future<void> createText(TextWidthBasis textWidthBasis) {
      return tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Center(
              child: Container(
                // Each word takes up more than a half of a line. Together they
                // wrap onto two lines, but leave a lot of extra space.
843
                child: Text(
844
                  'twowordsthateachtakeupmorethanhalfof alineoftextsothattheywrapwithlotsofextraspace',
845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
                  textDirection: TextDirection.ltr,
                  textWidthBasis: textWidthBasis,
                ),
              ),
            ),
          ),
        ),
      );
    }

    const double fontHeight = 14.0;
    const double screenWidth = 800.0;

    // When textWidthBasis is parent, takes up full screen width.
    await createText(TextWidthBasis.parent);
    final Size textSizeParent = tester.getSize(find.byType(Text));
    expect(textSizeParent.width, equals(screenWidth));
    expect(textSizeParent.height, equals(fontHeight * 2));

    // When textWidthBasis is longestLine, sets the width to as small as
    // possible for the two lines.
    await createText(TextWidthBasis.longestLine);
    final Size textSizeLongestLine = tester.getSize(find.byType(Text));
    expect(textSizeLongestLine.width, equals(630.0));
    expect(textSizeLongestLine.height, equals(fontHeight * 2));
870
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020
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 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
  testWidgets('textWidthBasis with textAlign still obeys parent alignment', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const <Widget>[
                Text(
                  'LEFT ALIGNED, PARENT',
                  textAlign: TextAlign.left,
                  textWidthBasis: TextWidthBasis.parent,
                ),
                Text(
                  'RIGHT ALIGNED, PARENT',
                  textAlign: TextAlign.right,
                  textWidthBasis: TextWidthBasis.parent,
                ),
                Text(
                  'LEFT ALIGNED, LONGEST LINE',
                  textAlign: TextAlign.left,
                  textWidthBasis: TextWidthBasis.longestLine,
                ),
                Text(
                  'RIGHT ALIGNED, LONGEST LINE',
                  textAlign: TextAlign.right,
                  textWidthBasis: TextWidthBasis.longestLine,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    // All Texts have the same horizontal alignment.
    final double offsetX = tester.getTopLeft(find.text('LEFT ALIGNED, PARENT')).dx;
    expect(tester.getTopLeft(find.text('RIGHT ALIGNED, PARENT')).dx, equals(offsetX));
    expect(tester.getTopLeft(find.text('LEFT ALIGNED, LONGEST LINE')).dx, equals(offsetX));
    expect(tester.getTopLeft(find.text('RIGHT ALIGNED, LONGEST LINE')).dx, equals(offsetX));

    // All Texts are less than or equal to the width of the Column.
    final double width = tester.getSize(find.byType(Column)).width;
    expect(tester.getSize(find.text('LEFT ALIGNED, PARENT')).width, lessThan(width));
    expect(tester.getSize(find.text('RIGHT ALIGNED, PARENT')).width, lessThan(width));
    expect(tester.getSize(find.text('LEFT ALIGNED, LONGEST LINE')).width, lessThan(width));
    expect(tester.getSize(find.text('RIGHT ALIGNED, LONGEST LINE')).width, equals(width));
920
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020
921

922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
  testWidgets(
    'textWidthBasis.longestLine confines the width of the paragraph '
    'when given loose constraints',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/62550.
      await tester.pumpWidget(
          Center(
            child: SizedBox(
              width: 400,
              child: Center(
                child: RichText(
                  text: const TextSpan(text: 'fwefwefwewfefewfwe fwfwfwefweabcdefghijklmnopqrstuvwxyz'),
                  textWidthBasis: TextWidthBasis.longestLine,
                  textDirection: TextDirection.ltr,
                ),
              ),
            ),
          ),
        );

      expect(find.byType(RichText), paints..something((Symbol method, List<dynamic> arguments) {
        if (method != #drawParagraph)
          return false;
        final ui.Paragraph paragraph = arguments[0] as ui.Paragraph;
946
        if (paragraph.longestLine > paragraph.width)
947 948 949 950 951
          throw 'paragraph width (${paragraph.width}) greater than its longest line (${paragraph.longestLine}).';
        if (paragraph.width >= 400)
          throw 'paragraph.width (${paragraph.width}) >= 400';
        return true;
      }));
952
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020
953

954 955 956 957 958 959 960
  testWidgets('Paragraph.getBoxesForRange returns nothing when selection range is zero length', (WidgetTester tester) async {
    final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle());
    builder.addText('hello');
    final ui.Paragraph paragraph = builder.build();
    paragraph.layout(const ui.ParagraphConstraints(width: 1000));
    expect(paragraph.getBoxesForRange(2, 2), isEmpty);
  });
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

  // Regression test for https://github.com/flutter/flutter/issues/65818
  testWidgets('WidgetSpans with no semantic information are elided from semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    // Without the fix for this bug the pump widget will throw a RangeError.
    await tester.pumpWidget(
      RichText(
        textDirection: TextDirection.ltr,
        text: TextSpan(children: <InlineSpan>[
          const WidgetSpan(child: SizedBox.shrink()),
          TextSpan(
            text: 'HELLO',
            style: const TextStyle(color: Colors.black),
            recognizer: TapGestureRecognizer()..onTap = () {},
          )
        ]),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics(
          id: 1,
          rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
          transform: Matrix4(
            3.0,0.0,0.0,0.0,
            0.0,3.0,0.0,0.0,
            0.0,0.0,1.0,0.0,
            0.0,0.0,0.0,1.0,
          ),
          children: <TestSemantics>[
            TestSemantics(
              rect: const Rect.fromLTRB(-4.0, -4.0, 74.0, 18.0),
              id: 2,
              label: 'HELLO',
              actions: <SemanticsAction>[
                SemanticsAction.tap,
              ],
              flags: <SemanticsFlag>[
                SemanticsFlag.isLink,
              ],
            ),
          ],
        ),
      ],
    )));
  }, semanticsEnabled: true, skip: isBrowser); // Browser semantics have different sizes.
1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025
}

Future<void> _pumpTextWidget({ WidgetTester tester, String text, TextOverflow overflow }) {
  return tester.pumpWidget(
    Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Container(
          width: 50.0,
          height: 50.0,
          child: Text(
            text,
            overflow: overflow,
          ),
        ),
      ),
    ),
  );
1026
}