paragraph_test.dart 25.3 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_test/flutter_test.dart';
11 12 13

import 'rendering_tester.dart';

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

16 17 18 19 20 21
// 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 {
22 23 24 25
  RenderParagraphWithEmptySelectionBoxList(
    InlineSpan text, {
    required TextDirection textDirection,
    required this.emptyListSelection,
26 27 28 29 30 31 32 33 34 35 36 37 38
  }) : super(text, textDirection: textDirection);

  TextSelection emptyListSelection;

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

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection
// for a selection representing a WidgetSpan.
// This is intended to simulate how SkParagraph's implementation of Paragraph.getBoxesForRange
// can return an empty list for a WidgetSpan with empty dimensions.
class RenderParagraphWithEmptyBoxListForWidgetSpan extends RenderParagraph {
  RenderParagraphWithEmptyBoxListForWidgetSpan(
    InlineSpan text, {
    required List<RenderBox> children,
    required TextDirection textDirection,
  }) : super(text, children: children, textDirection: textDirection);

  @override
  List<ui.TextBox> getBoxesForSelection(TextSelection selection) {
    if (text.getSpanForPosition(selection.base) is WidgetSpan) {
      return <ui.TextBox>[];
    }
    return super.getBoxesForSelection(selection);
  }
}

59 60
void main() {
  test('getOffsetForCaret control test', () {
61
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
62 63 64
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
65 66
    layout(paragraph);

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

69
    final Offset offset5 = paragraph.getOffsetForCaret(const TextPosition(offset: 5), caret);
70 71
    expect(offset5.dx, greaterThan(0.0));

72
    final Offset offset25 = paragraph.getOffsetForCaret(const TextPosition(offset: 25), caret);
73 74
    expect(offset25.dx, greaterThan(offset5.dx));

75
    final Offset offset50 = paragraph.getOffsetForCaret(const TextPosition(offset: 50), caret);
76 77 78
    expect(offset50.dy, greaterThan(offset5.dy));
  });

79 80 81 82 83 84 85
  test('getFullHeightForCaret control test', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(text: _kText,style: TextStyle(fontSize: 10.0)),
      textDirection: TextDirection.ltr,
    );
    layout(paragraph);

86
    final double height5 = paragraph.getFullHeightForCaret(const TextPosition(offset: 5))!;
87 88 89
    expect(height5, equals(10.0));
  });

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

97
    final TextPosition position20 = paragraph.getPositionForOffset(const Offset(20.0, 5.0));
98 99
    expect(position20.offset, greaterThan(0.0));

100
    final TextPosition position40 = paragraph.getPositionForOffset(const Offset(40.0, 5.0));
101 102
    expect(position40.offset, greaterThan(position20.offset));

103
    final TextPosition positionBelow = paragraph.getPositionForOffset(const Offset(5.0, 20.0));
104
    expect(positionBelow.offset, greaterThan(position40.offset));
105
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61015
106 107

  test('getBoxesForSelection control test', () {
108
    final RenderParagraph paragraph = RenderParagraph(
109
      const TextSpan(text: _kText, style: TextStyle(fontSize: 10.0)),
Ian Hickson's avatar
Ian Hickson committed
110 111
      textDirection: TextDirection.ltr,
    );
112 113 114
    layout(paragraph);

    List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
115
      const TextSelection(baseOffset: 5, extentOffset: 25),
116 117 118 119 120
    );

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

    boxes = paragraph.getBoxesForSelection(
121
      const TextSelection(baseOffset: 25, extentOffset: 50),
122 123
    );

124 125
    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);
126
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
127 128

  test('getWordBoundary control test', () {
129
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
130 131 132
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
133 134
    layout(paragraph);

135
    final TextRange range5 = paragraph.getWordBoundary(const TextPosition(offset: 5));
136 137
    expect(range5.textInside(_kText), equals('polished'));

138
    final TextRange range50 = paragraph.getWordBoundary(const TextPosition(offset: 50));
139 140
    expect(range50.textInside(_kText), equals(' '));

141
    final TextRange range85 = paragraph.getWordBoundary(const TextPosition(offset: 75));
142
    expect(range85.textInside(_kText), equals("Queen's"));
143
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61017
144 145

  test('overflow test', () {
146
    final RenderParagraph paragraph = RenderParagraph(
147 148 149
      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.',
150
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
151
      ),
Ian Hickson's avatar
Ian Hickson committed
152
      textDirection: TextDirection.ltr,
153 154 155 156
      maxLines: 1,
      softWrap: true,
    );

157 158 159 160 161
    void relayoutWith({
      int? maxLines,
      required bool softWrap,
      required TextOverflow overflow,
    }) {
162 163 164 165 166 167 168 169
      paragraph
        ..maxLines = maxLines
        ..softWrap = softWrap
        ..overflow = overflow;
      pumpFrame();
    }

    // Lay out in a narrow box to force wrapping.
170
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is"
171
    final double lineHeight = paragraph.size.height;
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

    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);
208
    expect(paragraph.size.height, equals(3 * lineHeight));
209 210 211

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));
212 213 214 215 216

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

217 218 219 220
    // Change back to ellipsis and check that the fade shader is cleared.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.debugHasOverflowShader, isFalse);

221 222
    relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isFalse);
223
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
224 225

  test('maxLines', () {
226
    final RenderParagraph paragraph = RenderParagraph(
227
      const TextSpan(
228
        text: "How do you write like you're running out of time? Write day and night like you're running out of time?",
229 230
            // 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
231
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
232
      ),
Ian Hickson's avatar
Ian Hickson committed
233
      textDirection: TextDirection.ltr,
234 235
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
236
    void layoutAt(int? maxLines) {
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
      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);
252
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
253

254
  test('changing color does not do layout', () {
255
    final RenderParagraph paragraph = RenderParagraph(
256 257
      const TextSpan(
        text: 'Hello',
258
        style: TextStyle(color: Color(0xFF000000)),
259
      ),
Ian Hickson's avatar
Ian Hickson committed
260
      textDirection: TextDirection.ltr,
261 262 263 264 265 266
    );
    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',
267
      style: TextStyle(color: Color(0xFF000000)),
268 269 270 271 272 273 274 275
    );
    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',
276
      style: TextStyle(color: Color(0xFFFFFFFF)),
277 278 279 280 281 282 283 284
    );
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isTrue);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
  });

285
  test('nested TextSpans in paragraph handle textScaleFactor correctly.', () {
286
    const TextSpan testSpan = TextSpan(
287
      text: 'a',
288
      style: TextStyle(
289 290
        fontSize: 10.0,
      ),
291 292
      children: <TextSpan>[
        TextSpan(
293
          text: 'b',
294 295
          children: <TextSpan>[
            TextSpan(text: 'c'),
296
          ],
297
          style: TextStyle(
298 299 300
            fontSize: 20.0,
          ),
        ),
301
        TextSpan(
302 303 304 305
          text: 'd',
        ),
      ],
    );
306
    final RenderParagraph paragraph = RenderParagraph(
307 308
        testSpan,
        textDirection: TextDirection.ltr,
309
        textScaleFactor: 1.3,
310 311 312 313
    );
    paragraph.layout(const BoxConstraints());
    // anyOf is needed here because Linux and Mac have different text
    // rendering widths in tests.
314
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
315 316 317 318 319
    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();
320 321 322
    final List<ui.TextBox> boxes = <ui.TextBox>[
      for (int i = 0; i < text.length; ++i)
        ...paragraph.getBoxesForSelection(
323
          TextSelection(baseOffset: i, extentOffset: i + 1),
324 325
        ),
    ];
326 327 328 329
    expect(boxes.length, equals(4));

    // anyOf is needed here and below because Linux and Mac have different text
    // rendering widths in tests.
330
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
331
    expect(boxes[0].toRect().width, anyOf(14.0, 13.0));
332
    expect(boxes[0].toRect().height, moreOrLessEquals(13.0, epsilon: 0.0001));
333
    expect(boxes[1].toRect().width, anyOf(27.0, 26.0));
334
    expect(boxes[1].toRect().height, moreOrLessEquals(26.0, epsilon: 0.0001));
335
    expect(boxes[2].toRect().width, anyOf(27.0, 26.0));
336
    expect(boxes[2].toRect().height, moreOrLessEquals(26.0, epsilon: 0.0001));
337
    expect(boxes[3].toRect().width, anyOf(14.0, 13.0));
338
    expect(boxes[3].toRect().height, moreOrLessEquals(13.0, epsilon: 0.0001));
339
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
340

341
  test('toStringDeep', () {
342
    final RenderParagraph paragraph = RenderParagraph(
343
      const TextSpan(text: _kText),
Ian Hickson's avatar
Ian Hickson committed
344
      textDirection: TextDirection.ltr,
345
      locale: const Locale('ja', 'JP'),
346
    );
347
    expect(paragraph, hasAGoodToStringDeep);
348
    expect(
349
      paragraph.toStringDeep(minLevel: DiagnosticLevel.info),
350 351
      equalsIgnoringHashCodes(
        'RenderParagraph#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n'
Ian Hickson's avatar
Ian Hickson committed
352 353
        ' │ parentData: MISSING\n'
        ' │ constraints: MISSING\n'
354
        ' │ size: MISSING\n'
Ian Hickson's avatar
Ian Hickson committed
355 356 357 358
        ' │ textAlign: start\n'
        ' │ textDirection: ltr\n'
        ' │ softWrap: wrapping at box width\n'
        ' │ overflow: clip\n'
359
        ' │ locale: ja_JP\n'
Ian Hickson's avatar
Ian Hickson committed
360
        ' │ maxLines: unlimited\n'
361 362 363
        ' ╘═╦══ text ═══\n'
        '   ║ TextSpan:\n'
        '   ║   "I polished up that handle so carefullee\n'
364
        '   ║   That now I am the Ruler of the Queen\'s Navee!"\n'
365
        '   ╚═══════════\n',
366 367 368
      ),
    );
  });
369 370 371 372

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

373
    final RenderParagraph paragraph = RenderParagraph(
374 375 376 377 378 379 380 381 382 383
      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'));
  });

384 385 386 387 388 389 390 391 392 393 394 395 396
  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.
397 398 399 400 401
    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),
    ];
402 403 404 405 406 407 408 409 410

    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(
411
      const TextSelection(baseOffset: 0, extentOffset: 8),
412 413 414 415 416 417 418 419
    );

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

422 423 424 425 426 427 428 429 430 431
  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> [
432 433
          WidgetSpan(child: Text(sentence)),
        ],
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
      ),
      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> [
450 451
          WidgetSpan(child: Text(sentence)),
        ],
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
      ),
      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> [
477 478
          WidgetSpan(child: Text(sentence)),
        ],
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
      ),
      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> [
495 496
          WidgetSpan(child: Text(sentence)),
        ],
497 498 499 500 501 502 503 504 505 506 507 508
      ),
      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

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
  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.
526 527 528 529 530 531 532 533 534
    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),
    ];
535 536 537 538 539 540 541 542 543

    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(
544
      const TextSelection(baseOffset: 0, extentOffset: 12),
545 546 547 548 549 550 551 552 553 554 555 556 557 558
    );

    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));
559
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
560 561 562 563 564 565 566 567 568 569 570 571 572

  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>[]);
573
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
574 575 576 577 578 579 580 581 582 583 584 585 586

  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>[]);
587
    } on AssertionError catch (e) {
588 589 590 591
      failed = true;
      expect(e.message, 'MultiTapGestureRecognizer is not supported.');
    }
    expect(failed, true);
592
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609

  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
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630

  test('assembleSemanticsNode handles empty WidgetSpans that do not yield selection boxes', () {
    final TextSpan text = TextSpan(text: '', children: <InlineSpan>[
      TextSpan(text: 'A', recognizer: TapGestureRecognizer()..onTap = () {}),
      const WidgetSpan(child: SizedBox(width: 0, height: 0)),
      TextSpan(text: 'C', recognizer: TapGestureRecognizer()..onTap = () {}),
    ]);
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
    final RenderParagraph paragraph = RenderParagraphWithEmptyBoxListForWidgetSpan(
      text,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph);

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