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

import 'dart:ui' as ui show TextBox;

7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10
import 'package:flutter/services.dart';
11
import 'package:flutter_test/flutter_test.dart';
12 13 14

import 'rendering_tester.dart';

15
const String _kText = "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen's Navee!";
16

17 18 19 20 21 22
// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection
// for a given TextSelection.
// This is intended to simulate SkParagraph's implementation of Paragraph.getBoxesForRange,
// which may return an empty list in some situations where Libtxt would return a list
// containing an empty box.
class RenderParagraphWithEmptySelectionBoxList extends RenderParagraph {
23 24 25 26
  RenderParagraphWithEmptySelectionBoxList(
    InlineSpan text, {
    required TextDirection textDirection,
    required this.emptyListSelection,
27 28 29 30 31 32 33 34 35 36 37 38 39
  }) : super(text, textDirection: textDirection);

  TextSelection emptyListSelection;

  @override
  List<ui.TextBox> getBoxesForSelection(TextSelection selection) {
    if (selection == emptyListSelection) {
      return <ui.TextBox>[];
    }
    return super.getBoxesForSelection(selection);
  }
}

40 41
void main() {
  test('getOffsetForCaret control test', () {
42
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
43 44 45
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
46 47
    layout(paragraph);

Dan Field's avatar
Dan Field committed
48
    const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
49

50
    final Offset offset5 = paragraph.getOffsetForCaret(const TextPosition(offset: 5), caret);
51 52
    expect(offset5.dx, greaterThan(0.0));

53
    final Offset offset25 = paragraph.getOffsetForCaret(const TextPosition(offset: 25), caret);
54 55
    expect(offset25.dx, greaterThan(offset5.dx));

56
    final Offset offset50 = paragraph.getOffsetForCaret(const TextPosition(offset: 50), caret);
57 58 59
    expect(offset50.dy, greaterThan(offset5.dy));
  });

60 61 62 63 64 65 66
  test('getFullHeightForCaret control test', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(text: _kText,style: TextStyle(fontSize: 10.0)),
      textDirection: TextDirection.ltr,
    );
    layout(paragraph);

67
    final double height5 = paragraph.getFullHeightForCaret(const TextPosition(offset: 5))!;
68 69 70
    expect(height5, equals(10.0));
  });

71
  test('getPositionForOffset control test', () {
72
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
73 74 75
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
76 77
    layout(paragraph);

78
    final TextPosition position20 = paragraph.getPositionForOffset(const Offset(20.0, 5.0));
79 80
    expect(position20.offset, greaterThan(0.0));

81
    final TextPosition position40 = paragraph.getPositionForOffset(const Offset(40.0, 5.0));
82 83
    expect(position40.offset, greaterThan(position20.offset));

84
    final TextPosition positionBelow = paragraph.getPositionForOffset(const Offset(5.0, 20.0));
85
    expect(positionBelow.offset, greaterThan(position40.offset));
86
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61015
87 88

  test('getBoxesForSelection control test', () {
89
    final RenderParagraph paragraph = RenderParagraph(
90
      const TextSpan(text: _kText, style: TextStyle(fontSize: 10.0)),
Ian Hickson's avatar
Ian Hickson committed
91 92
      textDirection: TextDirection.ltr,
    );
93 94 95
    layout(paragraph);

    List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
96
        const TextSelection(baseOffset: 5, extentOffset: 25)
97 98 99 100 101
    );

    expect(boxes.length, equals(1));

    boxes = paragraph.getBoxesForSelection(
102
        const TextSelection(baseOffset: 25, extentOffset: 50)
103 104
    );

105 106
    expect(boxes.any((ui.TextBox box) => box.left == 250 && box.top == 0), isTrue);
    expect(boxes.any((ui.TextBox box) => box.right == 100 && box.top == 10), isTrue);
107
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
108 109

  test('getWordBoundary control test', () {
110
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
111 112 113
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
114 115
    layout(paragraph);

116
    final TextRange range5 = paragraph.getWordBoundary(const TextPosition(offset: 5));
117 118
    expect(range5.textInside(_kText), equals('polished'));

119
    final TextRange range50 = paragraph.getWordBoundary(const TextPosition(offset: 50));
120 121
    expect(range50.textInside(_kText), equals(' '));

122
    final TextRange range85 = paragraph.getWordBoundary(const TextPosition(offset: 75));
123
    expect(range85.textInside(_kText), equals("Queen's"));
124
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61017
125 126

  test('overflow test', () {
127
    final RenderParagraph paragraph = RenderParagraph(
128 129 130
      const TextSpan(
        text: 'This\n' // 4 characters * 10px font size = 40px width on the first line
              'is a wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.',
131
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
132
      ),
Ian Hickson's avatar
Ian Hickson committed
133
      textDirection: TextDirection.ltr,
134 135 136 137
      maxLines: 1,
      softWrap: true,
    );

138 139 140 141 142
    void relayoutWith({
      int? maxLines,
      required bool softWrap,
      required TextOverflow overflow,
    }) {
143 144 145 146 147 148 149 150
      paragraph
        ..maxLines = maxLines
        ..softWrap = softWrap
        ..overflow = overflow;
      pumpFrame();
    }

    // Lay out in a narrow box to force wrapping.
151
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is"
152
    final double lineHeight = paragraph.size.height;
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188

    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(3 * lineHeight));

    relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.clip);
    expect(paragraph.size.height, greaterThan(5 * lineHeight));

    // Try again with ellipsis overflow. We can't test that the ellipsis are
    // drawn, but we can test the sizing.
    relayoutWith(maxLines: 1, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(3 * lineHeight));

    // This is the one weird case. If maxLines is null, we would expect to allow
    // infinite wrapping. However, if we did, we'd never know when to append an
    // ellipsis, so this really means "append ellipsis as soon as we exceed the
    // width".
    relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));

    // Now with no soft wrapping.
    relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(2 * lineHeight));

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(2 * lineHeight));

    relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.ellipsis);
189
    expect(paragraph.size.height, equals(3 * lineHeight));
190 191 192

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));
193 194 195 196 197

    // Test presence of the fade effect.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isTrue);

198 199 200 201
    // Change back to ellipsis and check that the fade shader is cleared.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.debugHasOverflowShader, isFalse);

202 203
    relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isFalse);
204
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
205 206

  test('maxLines', () {
207
    final RenderParagraph paragraph = RenderParagraph(
208
      const TextSpan(
209
        text: "How do you write like you're running out of time? Write day and night like you're running out of time?",
210 211
            // 0123456789 0123456789 012 345 0123456 012345 01234 012345678 012345678 0123 012 345 0123456 012345 01234
            // 0          1          2       3       4      5     6         7         8    9       10      11     12
212
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
213
      ),
Ian Hickson's avatar
Ian Hickson committed
214
      textDirection: TextDirection.ltr,
215 216
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
217
    void layoutAt(int? maxLines) {
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
      paragraph.maxLines = maxLines;
      pumpFrame();
    }

    layoutAt(null);
    expect(paragraph.size.height, 130.0);

    layoutAt(1);
    expect(paragraph.size.height, 10.0);

    layoutAt(2);
    expect(paragraph.size.height, 20.0);

    layoutAt(3);
    expect(paragraph.size.height, 30.0);
233
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
234

235
  test('changing color does not do layout', () {
236
    final RenderParagraph paragraph = RenderParagraph(
237 238
      const TextSpan(
        text: 'Hello',
239
        style: TextStyle(color: Color(0xFF000000)),
240
      ),
Ian Hickson's avatar
Ian Hickson committed
241
      textDirection: TextDirection.ltr,
242 243 244 245 246 247
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0), phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
    paragraph.text = const TextSpan(
      text: 'Hello World',
248
      style: TextStyle(color: Color(0xFF000000)),
249 250 251 252 253 254 255 256
    );
    expect(paragraph.debugNeedsLayout, isTrue);
    expect(paragraph.debugNeedsPaint, isFalse);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
    paragraph.text = const TextSpan(
      text: 'Hello World',
257
      style: TextStyle(color: Color(0xFFFFFFFF)),
258 259 260 261 262 263 264 265
    );
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isTrue);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
  });

266
  test('nested TextSpans in paragraph handle textScaleFactor correctly.', () {
267
    const TextSpan testSpan = TextSpan(
268
      text: 'a',
269
      style: TextStyle(
270 271
        fontSize: 10.0,
      ),
272 273
      children: <TextSpan>[
        TextSpan(
274
          text: 'b',
275 276
          children: <TextSpan>[
            TextSpan(text: 'c'),
277
          ],
278
          style: TextStyle(
279 280 281
            fontSize: 20.0,
          ),
        ),
282
        TextSpan(
283 284 285 286
          text: 'd',
        ),
      ],
    );
287
    final RenderParagraph paragraph = RenderParagraph(
288 289
        testSpan,
        textDirection: TextDirection.ltr,
290
        textScaleFactor: 1.3,
291 292 293 294
    );
    paragraph.layout(const BoxConstraints());
    // anyOf is needed here because Linux and Mac have different text
    // rendering widths in tests.
295
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
296 297 298 299 300
    expect(paragraph.size.width, anyOf(79.0, 78.0));
    expect(paragraph.size.height, 26.0);

    // Test the sizes of nested spans.
    final String text = testSpan.toStringDeep();
301 302 303
    final List<ui.TextBox> boxes = <ui.TextBox>[
      for (int i = 0; i < text.length; ++i)
        ...paragraph.getBoxesForSelection(
304
          TextSelection(baseOffset: i, extentOffset: i + 1)
305 306
        ),
    ];
307 308 309 310
    expect(boxes.length, equals(4));

    // anyOf is needed here and below because Linux and Mac have different text
    // rendering widths in tests.
311
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
312
    expect(boxes[0].toRect().width, anyOf(14.0, 13.0));
313
    expect(boxes[0].toRect().height, moreOrLessEquals(13.0, epsilon: 0.0001));
314
    expect(boxes[1].toRect().width, anyOf(27.0, 26.0));
315
    expect(boxes[1].toRect().height, moreOrLessEquals(26.0, epsilon: 0.0001));
316
    expect(boxes[2].toRect().width, anyOf(27.0, 26.0));
317
    expect(boxes[2].toRect().height, moreOrLessEquals(26.0, epsilon: 0.0001));
318
    expect(boxes[3].toRect().width, anyOf(14.0, 13.0));
319
    expect(boxes[3].toRect().height, moreOrLessEquals(13.0, epsilon: 0.0001));
320
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
321

322
  test('toStringDeep', () {
323
    final RenderParagraph paragraph = RenderParagraph(
324
      const TextSpan(text: _kText),
Ian Hickson's avatar
Ian Hickson committed
325
      textDirection: TextDirection.ltr,
326
      locale: const Locale('ja', 'JP'),
327
    );
328
    expect(paragraph, hasAGoodToStringDeep);
329
    expect(
330
      paragraph.toStringDeep(minLevel: DiagnosticLevel.info),
331 332
      equalsIgnoringHashCodes(
        'RenderParagraph#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n'
Ian Hickson's avatar
Ian Hickson committed
333 334
        ' │ parentData: MISSING\n'
        ' │ constraints: MISSING\n'
335
        ' │ size: MISSING\n'
Ian Hickson's avatar
Ian Hickson committed
336 337 338 339
        ' │ textAlign: start\n'
        ' │ textDirection: ltr\n'
        ' │ softWrap: wrapping at box width\n'
        ' │ overflow: clip\n'
340
        ' │ locale: ja_JP\n'
Ian Hickson's avatar
Ian Hickson committed
341
        ' │ maxLines: unlimited\n'
342 343 344
        ' ╘═╦══ text ═══\n'
        '   ║ TextSpan:\n'
        '   ║   "I polished up that handle so carefullee\n'
345
        '   ║   That now I am the Ruler of the Queen\'s Navee!"\n'
346 347 348 349
        '   ╚═══════════\n'
      ),
    );
  });
350 351 352 353

  test('locale setter', () {
    // Regression test for https://github.com/flutter/flutter/issues/18175

354
    final RenderParagraph paragraph = RenderParagraph(
355 356 357 358 359 360 361 362 363 364
      const TextSpan(text: _kText),
      locale: const Locale('zh', 'HK'),
      textDirection: TextDirection.ltr,
    );
    expect(paragraph.locale, const Locale('zh', 'HK'));

    paragraph.locale = const Locale('ja', 'JP');
    expect(paragraph.locale, const Locale('ja', 'JP'));
  });

365 366 367 368 369 370 371 372 373 374 375 376 377
  test('inline widgets test', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce dependencies this test has.
378 379 380 381 382
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 8)
    );

    expect(boxes.length, equals(5));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
    expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr));
401
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
402

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 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
  test('can compute IntrinsicHeight for widget span', () {
    // Regression test for https://github.com/flutter/flutter/issues/59316
    const double screenWidth = 100.0;
    const String sentence = 'one two';
    List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
          WidgetSpan(child: Text(sentence))
        ]
      ),
      textScaleFactor: 1.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double singleLineHeight = paragraph.computeMaxIntrinsicHeight(screenWidth);
    expect(singleLineHeight, 14.0);

    pumpFrame();
    renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
          WidgetSpan(child: Text(sentence))
        ]
      ),
      textScaleFactor: 2.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );

    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double maxIntrinsicHeight = paragraph.computeMaxIntrinsicHeight(screenWidth);
    final double minIntrinsicHeight = paragraph.computeMinIntrinsicHeight(screenWidth);
    // intrinsicHeight = singleLineHeight * textScaleFactor * two lines.
    expect(maxIntrinsicHeight, singleLineHeight * 2.0 * 2);
    expect(maxIntrinsicHeight, minIntrinsicHeight);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020

  test('can compute IntrinsicWidth for widget span', () {
    // Regression test for https://github.com/flutter/flutter/issues/59316
    const double screenWidth = 1000.0;
    const double fixedHeight = 1000.0;
    const String sentence = 'one two';
    List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
          WidgetSpan(child: Text(sentence))
        ]
      ),
      textScaleFactor: 1.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double widthForOneLine = paragraph.computeMaxIntrinsicWidth(fixedHeight);
    expect(widthForOneLine, 98.0);

    pumpFrame();
    renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
          WidgetSpan(child: Text(sentence))
        ]
      ),
      textScaleFactor: 2.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );

    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double maxIntrinsicWidth = paragraph.computeMaxIntrinsicWidth(fixedHeight);
    // maxIntrinsicWidth = widthForOneLine * textScaleFactor
    expect(maxIntrinsicWidth, widthForOneLine * 2.0);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
  test('inline widgets multiline test', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce dependencies this test has.
507 508 509 510 511 512 513 514 515
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 12)
    );

    expect(boxes.length, equals(9));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
    // Wraps
    expect(boxes[4], const TextBox.fromLTRBD(0.0, 14.0, 14.0, 28.0 , TextDirection.ltr));
    expect(boxes[5], const TextBox.fromLTRBD(14.0, 14.0, 28.0, 28.0, TextDirection.ltr));
    expect(boxes[6], const TextBox.fromLTRBD(28.0, 14.0, 42.0, 28.0, TextDirection.ltr));
    // Wraps
    expect(boxes[7], const TextBox.fromLTRBD(0.0, 28.0, 14.0, 42.0, TextDirection.ltr));
    expect(boxes[8], const TextBox.fromLTRBD(14.0, 28.0, 28.0, 42.0 , TextDirection.ltr));
540
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
541 542 543 544 545 546 547 548 549 550 551 552 553

  test('Supports gesture recognizer semantics', () {
    final RenderParagraph paragraph = RenderParagraph(
      TextSpan(text: _kText, children: <InlineSpan>[
        TextSpan(text: 'one', recognizer: TapGestureRecognizer()..onTap = () {}),
        TextSpan(text: 'two', recognizer: LongPressGestureRecognizer()..onLongPress = () {}),
        TextSpan(text: 'three', recognizer: DoubleTapGestureRecognizer()..onDoubleTap = () {}),
      ]),
      textDirection: TextDirection.rtl,
    );
    layout(paragraph);

    paragraph.assembleSemanticsNode(SemanticsNode(), SemanticsConfiguration(), <SemanticsNode>[]);
554
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
555 556 557 558 559 560 561 562 563 564 565 566 567

  test('Asserts on unsupported gesture recognizer', () {
    final RenderParagraph paragraph = RenderParagraph(
      TextSpan(text: _kText, children: <InlineSpan>[
        TextSpan(text: 'three', recognizer: MultiTapGestureRecognizer()..onTap = (int id) {}),
      ]),
      textDirection: TextDirection.rtl,
    );
    layout(paragraph);

    bool failed = false;
    try {
      paragraph.assembleSemanticsNode(SemanticsNode(), SemanticsConfiguration(), <SemanticsNode>[]);
568
    } on AssertionError catch (e) {
569 570 571 572
      failed = true;
      expect(e.message, 'MultiTapGestureRecognizer is not supported.');
    }
    expect(failed, true);
573
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590

  test('assembleSemanticsNode handles text spans that do not yield selection boxes', () {
    final RenderParagraph paragraph = RenderParagraphWithEmptySelectionBoxList(
      TextSpan(text: '', children: <InlineSpan>[
        TextSpan(text: 'A', recognizer: TapGestureRecognizer()..onTap = () {}),
        TextSpan(text: 'B', recognizer: TapGestureRecognizer()..onTap = () {}),
        TextSpan(text: 'C', recognizer: TapGestureRecognizer()..onTap = () {}),
      ]),
      textDirection: TextDirection.rtl,
      emptyListSelection: const TextSelection(baseOffset: 0, extentOffset: 1),
    );
    layout(paragraph);

    final SemanticsNode node = SemanticsNode();
    paragraph.assembleSemanticsNode(node, SemanticsConfiguration(), <SemanticsNode>[]);
    expect(node.childrenCount, 2);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
591
}