paragraph_test.dart 21.5 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 show TextBox;

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

import 'rendering_tester.dart';

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

void main() {
  test('getOffsetForCaret control test', () {
21
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
22 23 24
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
25 26
    layout(paragraph);

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

29
    final Offset offset5 = paragraph.getOffsetForCaret(const TextPosition(offset: 5), caret);
30 31
    expect(offset5.dx, greaterThan(0.0));

32
    final Offset offset25 = paragraph.getOffsetForCaret(const TextPosition(offset: 25), caret);
33 34
    expect(offset25.dx, greaterThan(offset5.dx));

35
    final Offset offset50 = paragraph.getOffsetForCaret(const TextPosition(offset: 50), caret);
36 37 38 39
    expect(offset50.dy, greaterThan(offset5.dy));
  });

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

46
    final TextPosition position20 = paragraph.getPositionForOffset(const Offset(20.0, 5.0));
47 48
    expect(position20.offset, greaterThan(0.0));

49
    final TextPosition position40 = paragraph.getPositionForOffset(const Offset(40.0, 5.0));
50 51
    expect(position40.offset, greaterThan(position20.offset));

52
    final TextPosition positionBelow = paragraph.getPositionForOffset(const Offset(5.0, 20.0));
53
    expect(positionBelow.offset, greaterThan(position40.offset));
54
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61015
55 56

  test('getBoxesForSelection control test', () {
57
    final RenderParagraph paragraph = RenderParagraph(
58
      const TextSpan(text: _kText, style: TextStyle(fontSize: 10.0)),
Ian Hickson's avatar
Ian Hickson committed
59 60
      textDirection: TextDirection.ltr,
    );
61 62 63
    layout(paragraph);

    List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
64
        const TextSelection(baseOffset: 5, extentOffset: 25)
65 66 67 68 69
    );

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

    boxes = paragraph.getBoxesForSelection(
70
        const TextSelection(baseOffset: 25, extentOffset: 50)
71 72
    );

73 74
    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);
75
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
76 77

  test('getWordBoundary control test', () {
78
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
79 80 81
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
82 83
    layout(paragraph);

84
    final TextRange range5 = paragraph.getWordBoundary(const TextPosition(offset: 5));
85 86
    expect(range5.textInside(_kText), equals('polished'));

87
    final TextRange range50 = paragraph.getWordBoundary(const TextPosition(offset: 50));
88 89
    expect(range50.textInside(_kText), equals(' '));

90
    final TextRange range85 = paragraph.getWordBoundary(const TextPosition(offset: 75));
91
    expect(range85.textInside(_kText), equals("Queen's"));
92
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61017
93 94

  test('overflow test', () {
95
    final RenderParagraph paragraph = RenderParagraph(
96 97 98
      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.',
99
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
100
      ),
Ian Hickson's avatar
Ian Hickson committed
101
      textDirection: TextDirection.ltr,
102 103 104 105
      maxLines: 1,
      softWrap: true,
    );

106
    void relayoutWith({ int maxLines, bool softWrap, TextOverflow overflow }) {
107 108 109 110 111 112 113 114
      paragraph
        ..maxLines = maxLines
        ..softWrap = softWrap
        ..overflow = overflow;
      pumpFrame();
    }

    // Lay out in a narrow box to force wrapping.
115
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is"
116
    final double lineHeight = paragraph.size.height;
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

    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);
153
    expect(paragraph.size.height, equals(3 * lineHeight));
154 155 156

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));
157 158 159 160 161

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

162 163 164 165
    // Change back to ellipsis and check that the fade shader is cleared.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.debugHasOverflowShader, isFalse);

166 167
    relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isFalse);
168
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
169 170

  test('maxLines', () {
171
    final RenderParagraph paragraph = RenderParagraph(
172
      const TextSpan(
173
        text: "How do you write like you're running out of time? Write day and night like you're running out of time?",
174 175
            // 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
176
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
177
      ),
Ian Hickson's avatar
Ian Hickson committed
178
      textDirection: TextDirection.ltr,
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
    void layoutAt(int maxLines) {
      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);
197
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
198

199
  test('changing color does not do layout', () {
200
    final RenderParagraph paragraph = RenderParagraph(
201 202
      const TextSpan(
        text: 'Hello',
203
        style: TextStyle(color: Color(0xFF000000)),
204
      ),
Ian Hickson's avatar
Ian Hickson committed
205
      textDirection: TextDirection.ltr,
206 207 208 209 210 211
    );
    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',
212
      style: TextStyle(color: Color(0xFF000000)),
213 214 215 216 217 218 219 220
    );
    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',
221
      style: TextStyle(color: Color(0xFFFFFFFF)),
222 223 224 225 226 227 228 229
    );
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isTrue);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
  });

230
  test('nested TextSpans in paragraph handle textScaleFactor correctly.', () {
231
    const TextSpan testSpan = TextSpan(
232
      text: 'a',
233
      style: TextStyle(
234 235
        fontSize: 10.0,
      ),
236 237
      children: <TextSpan>[
        TextSpan(
238
          text: 'b',
239 240
          children: <TextSpan>[
            TextSpan(text: 'c'),
241
          ],
242
          style: TextStyle(
243 244 245
            fontSize: 20.0,
          ),
        ),
246
        TextSpan(
247 248 249 250
          text: 'd',
        ),
      ],
    );
251
    final RenderParagraph paragraph = RenderParagraph(
252 253
        testSpan,
        textDirection: TextDirection.ltr,
254
        textScaleFactor: 1.3,
255 256 257 258
    );
    paragraph.layout(const BoxConstraints());
    // anyOf is needed here because Linux and Mac have different text
    // rendering widths in tests.
259
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
260 261 262 263 264
    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();
265 266 267
    final List<ui.TextBox> boxes = <ui.TextBox>[
      for (int i = 0; i < text.length; ++i)
        ...paragraph.getBoxesForSelection(
268
          TextSelection(baseOffset: i, extentOffset: i + 1)
269 270
        ),
    ];
271 272 273 274
    expect(boxes.length, equals(4));

    // anyOf is needed here and below because Linux and Mac have different text
    // rendering widths in tests.
275
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
276
    expect(boxes[0].toRect().width, anyOf(14.0, 13.0));
277
    expect(boxes[0].toRect().height, closeTo(13.0, 0.0001));
278
    expect(boxes[1].toRect().width, anyOf(27.0, 26.0));
279
    expect(boxes[1].toRect().height, closeTo(26.0, 0.0001));
280
    expect(boxes[2].toRect().width, anyOf(27.0, 26.0));
281
    expect(boxes[2].toRect().height, closeTo(26.0, 0.0001));
282
    expect(boxes[3].toRect().width, anyOf(14.0, 13.0));
283
    expect(boxes[3].toRect().height, closeTo(13.0, 0.0001));
284
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
285

286
  test('toStringDeep', () {
287
    final RenderParagraph paragraph = RenderParagraph(
288
      const TextSpan(text: _kText),
Ian Hickson's avatar
Ian Hickson committed
289
      textDirection: TextDirection.ltr,
290
      locale: const Locale('ja', 'JP'),
291
    );
292
    expect(paragraph, hasAGoodToStringDeep);
293
    expect(
294
      paragraph.toStringDeep(minLevel: DiagnosticLevel.info),
295 296
      equalsIgnoringHashCodes(
        'RenderParagraph#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n'
Ian Hickson's avatar
Ian Hickson committed
297 298
        ' │ parentData: MISSING\n'
        ' │ constraints: MISSING\n'
299
        ' │ size: MISSING\n'
Ian Hickson's avatar
Ian Hickson committed
300 301 302 303
        ' │ textAlign: start\n'
        ' │ textDirection: ltr\n'
        ' │ softWrap: wrapping at box width\n'
        ' │ overflow: clip\n'
304
        ' │ locale: ja_JP\n'
Ian Hickson's avatar
Ian Hickson committed
305
        ' │ maxLines: unlimited\n'
306 307 308
        ' ╘═╦══ text ═══\n'
        '   ║ TextSpan:\n'
        '   ║   "I polished up that handle so carefullee\n'
309
        '   ║   That now I am the Ruler of the Queen\'s Navee!"\n'
310 311 312 313
        '   ╚═══════════\n'
      ),
    );
  });
314 315 316 317

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

318
    final RenderParagraph paragraph = RenderParagraph(
319 320 321 322 323 324 325 326 327 328
      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'));
  });

329 330 331 332 333 334 335 336 337 338 339 340 341
  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.
342 343 344 345 346
    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),
    ];
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364

    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));
365
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
366

367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 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
  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

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
  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.
471 472 473 474 475 476 477 478 479
    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),
    ];
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503

    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));
504
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
505 506 507 508 509 510 511 512 513 514 515 516 517

  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>[]);
518
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536

  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>[]);
    } catch(e) {
      failed = true;
      expect(e.message, 'MultiTapGestureRecognizer is not supported.');
    }
    expect(failed, true);
537
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
538
}