semantics_test.dart 32.3 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 7
import 'dart:math';
import 'dart:ui';

8
import 'package:flutter/material.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/semantics.dart';
11 12 13
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

14
import 'semantics_tester.dart';
15 16

void main() {
17 18 19 20
  setUp(() {
    debugResetSemanticsIdCounter();
  });

21
  testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
22
    SemanticsTester semantics = SemanticsTester(tester);
23

24
    final TestSemantics expectedSemantics = TestSemantics.root(
25
      children: <TestSemantics>[
26
        TestSemantics.rootChild(
27 28
          label: 'test1',
          textDirection: TextDirection.ltr,
29
        ),
30
      ],
31
    );
32 33

    await tester.pumpWidget(
34 35
      Container(
        child: Semantics(
36
          label: 'test1',
Ian Hickson's avatar
Ian Hickson committed
37
          textDirection: TextDirection.ltr,
38 39
          child: Container(),
        ),
40
      ),
41 42
    );

43 44 45 46 47 48
    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
49

50 51
    semantics.dispose();
    semantics = null;
52 53

    expect(tester.binding.hasScheduledFrame, isFalse);
54
    semantics = SemanticsTester(tester);
55 56 57
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();

58 59 60 61 62 63
    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
64
    semantics.dispose();
65
  }, semanticsEnabled: false);
66 67

  testWidgets('Detach and reattach assert', (WidgetTester tester) async {
68 69
    final SemanticsTester semantics = SemanticsTester(tester);
    final GlobalKey key = GlobalKey();
70

71
    await tester.pumpWidget(Directionality(
Ian Hickson's avatar
Ian Hickson committed
72
      textDirection: TextDirection.ltr,
73 74
      child: Container(
        child: Semantics(
75
          label: 'test1',
76
          child: Semantics(
77 78 79
            key: key,
            container: true,
            label: 'test2a',
80 81 82 83
            child: Container(),
          ),
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
84
    ));
85

86
    expect(semantics, hasSemantics(
87
      TestSemantics.root(
88
        children: <TestSemantics>[
89
          TestSemantics.rootChild(
90 91
            label: 'test1',
            children: <TestSemantics>[
92
              TestSemantics(
93
                label: 'test2a',
94 95 96
              ),
            ],
          ),
97
        ],
98 99 100 101
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
102
    ));
103

104
    await tester.pumpWidget(Directionality(
Ian Hickson's avatar
Ian Hickson committed
105
      textDirection: TextDirection.ltr,
106 107
      child: Container(
        child: Semantics(
108
          label: 'test1',
109
          child: Semantics(
110 111
            container: true,
            label: 'middle',
112
            child: Semantics(
113 114 115
              key: key,
              container: true,
              label: 'test2b',
116 117 118 119 120
              child: Container(),
            ),
          ),
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
121
    ));
122

123
    expect(semantics, hasSemantics(
124
      TestSemantics.root(
125
        children: <TestSemantics>[
126
          TestSemantics.rootChild(
127 128
            label: 'test1',
            children: <TestSemantics>[
129
              TestSemantics(
130
                label: 'middle',
131
                children: <TestSemantics>[
132
                  TestSemantics(
133 134 135
                    label: 'test2b',
                  ),
                ],
136 137 138
              ),
            ],
          ),
139
        ],
140 141 142 143
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
144
    ));
145

146
    semantics.dispose();
147
  });
Ian Hickson's avatar
Ian Hickson committed
148 149

  testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async {
150
    final SemanticsTester semantics = SemanticsTester(tester);
Ian Hickson's avatar
Ian Hickson committed
151 152

    await tester.pumpWidget(
153
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
154
        textDirection: TextDirection.rtl,
155
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
156
          label: 'test1',
157
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
158 159 160 161
        ),
      ),
    );

162
    expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl));
163
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
164 165 166
  });

  testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async {
167
    final SemanticsTester semantics = SemanticsTester(tester);
Ian Hickson's avatar
Ian Hickson committed
168 169

    await tester.pumpWidget(
170
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
171
        textDirection: TextDirection.ltr,
172
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
173
          label: 'test1',
174
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
175 176 177 178
        ),
      ),
    );

179
    expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr));
180
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
181 182
  });

183
  testWidgets('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async {
184
    final SemanticsTester semantics = SemanticsTester(tester);
Ian Hickson's avatar
Ian Hickson committed
185

186
    final TestSemantics expectedSemantics = TestSemantics.root(
187
      children: <TestSemantics>[
188
        TestSemantics.rootChild(
189 190
          label: 'test1',
          textDirection: TextDirection.ltr,
191
        ),
192
      ],
Ian Hickson's avatar
Ian Hickson committed
193 194 195
    );

    await tester.pumpWidget(
196
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
197
        textDirection: TextDirection.rtl,
198
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
199 200
          label: 'test1',
          textDirection: TextDirection.ltr,
201
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
202 203 204 205
        ),
      ),
    );

206
    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
207
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
208 209
  });

210
  testWidgets('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async {
211
    final SemanticsTester semantics = SemanticsTester(tester);
Ian Hickson's avatar
Ian Hickson committed
212

213
    final TestSemantics expectedSemantics = TestSemantics.root(
214
      children: <TestSemantics>[
215
        TestSemantics.rootChild(
216 217
          label: 'test1',
          textDirection: TextDirection.rtl,
218
        ),
219
      ],
Ian Hickson's avatar
Ian Hickson committed
220 221 222
    );

    await tester.pumpWidget(
223
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
224
        textDirection: TextDirection.ltr,
225
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
226 227
          label: 'test1',
          textDirection: TextDirection.rtl,
228
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
229 230 231 232
        ),
      ),
    );

233
    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
234
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
235
  });
236 237

  testWidgets('Semantics label and hint', (WidgetTester tester) async {
238
    final SemanticsTester semantics = SemanticsTester(tester);
239 240

    await tester.pumpWidget(
241
      Directionality(
242
        textDirection: TextDirection.ltr,
243
        child: Semantics(
244 245 246
          label: 'label',
          hint: 'hint',
          value: 'value',
247
          child: Container(),
248 249 250 251
        ),
      ),
    );

252
    final TestSemantics expectedSemantics = TestSemantics.root(
253
      children: <TestSemantics>[
254
        TestSemantics.rootChild(
255 256 257 258
          label: 'label',
          hint: 'hint',
          value: 'value',
          textDirection: TextDirection.ltr,
259
        ),
260
      ],
261 262 263
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
264
    semantics.dispose();
265 266 267
  });

  testWidgets('Semantics hints can merge', (WidgetTester tester) async {
268
    final SemanticsTester semantics = SemanticsTester(tester);
269 270

    await tester.pumpWidget(
271
      Directionality(
272
        textDirection: TextDirection.ltr,
273
        child: Semantics(
274
          container: true,
275
          child: Column(
276
            children: <Widget>[
277
              Semantics(
278 279
                hint: 'hint one',
              ),
280
              Semantics(
281
                hint: 'hint two',
282
              ),
283 284 285 286 287 288 289

            ],
          ),
        ),
      ),
    );

290
    final TestSemantics expectedSemantics = TestSemantics.root(
291
      children: <TestSemantics>[
292
        TestSemantics.rootChild(
293 294
          hint: 'hint one\nhint two',
          textDirection: TextDirection.ltr,
295
        ),
296
      ],
297 298 299
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
300
    semantics.dispose();
301 302 303
  });

  testWidgets('Semantics values do not merge', (WidgetTester tester) async {
304
    final SemanticsTester semantics = SemanticsTester(tester);
305 306

    await tester.pumpWidget(
307
      Directionality(
308
        textDirection: TextDirection.ltr,
309
        child: Semantics(
310
          container: true,
311
          child: Column(
312
            children: <Widget>[
313
              Semantics(
314
                value: 'value one',
315
                child: Container(
316 317
                  height: 10.0,
                  width: 10.0,
318
                ),
319
              ),
320
              Semantics(
321
                value: 'value two',
322
                child: Container(
323 324
                  height: 10.0,
                  width: 10.0,
325 326
                ),
              ),
327 328 329 330 331 332
            ],
          ),
        ),
      ),
    );

333
    final TestSemantics expectedSemantics = TestSemantics.root(
334
      children: <TestSemantics>[
335
        TestSemantics.rootChild(
336
          children: <TestSemantics>[
337
            TestSemantics(
338 339 340
              value: 'value one',
              textDirection: TextDirection.ltr,
            ),
341
            TestSemantics(
342 343 344
              value: 'value two',
              textDirection: TextDirection.ltr,
            ),
345
          ],
346
        ),
347 348 349 350
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
351
    semantics.dispose();
352 353 354
  });

  testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
355
    final SemanticsTester semantics = SemanticsTester(tester);
356 357

    await tester.pumpWidget(
358
      Directionality(
359
        textDirection: TextDirection.ltr,
360
        child: Semantics(
361
          container: true,
362
          child: Column(
363
            children: <Widget>[
364
              Semantics(
365 366
                hint: 'hint',
              ),
367
              Semantics(
368 369 370 371 372 373 374 375
                value: 'value',
              ),
            ],
          ),
        ),
      ),
    );

376
    final TestSemantics expectedSemantics = TestSemantics.root(
377
      children: <TestSemantics>[
378
        TestSemantics.rootChild(
379 380 381
          hint: 'hint',
          value: 'value',
          textDirection: TextDirection.ltr,
382
        ),
383
      ],
384 385 386
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
387
    semantics.dispose();
388
  });
389 390

  testWidgets('Semantics widget supports all actions', (WidgetTester tester) async {
391
    final SemanticsTester semantics = SemanticsTester(tester);
392 393 394 395

    final List<SemanticsAction> performedActions = <SemanticsAction>[];

    await tester.pumpWidget(
396
      Semantics(
397
        container: true,
398
        onDismiss: () => performedActions.add(SemanticsAction.dismiss),
399 400 401 402 403 404 405 406
        onTap: () => performedActions.add(SemanticsAction.tap),
        onLongPress: () => performedActions.add(SemanticsAction.longPress),
        onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
        onScrollRight: () => performedActions.add(SemanticsAction.scrollRight),
        onScrollUp: () => performedActions.add(SemanticsAction.scrollUp),
        onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
        onIncrease: () => performedActions.add(SemanticsAction.increase),
        onDecrease: () => performedActions.add(SemanticsAction.decrease),
407 408 409
        onCopy: () => performedActions.add(SemanticsAction.copy),
        onCut: () => performedActions.add(SemanticsAction.cut),
        onPaste: () => performedActions.add(SemanticsAction.paste),
410 411
        onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
        onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
412
        onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
413 414
        onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
        onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
415
      ),
416 417 418
    );

    final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
419 420
      ..remove(SemanticsAction.moveCursorForwardByWord)
      ..remove(SemanticsAction.moveCursorBackwardByWord)
421
      ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
422
      ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
423

424
    const int expectedId = 1;
425
    final TestSemantics expectedSemantics = TestSemantics.root(
426
      children: <TestSemantics>[
427
        TestSemantics.rootChild(
428 429
          id: expectedId,
          rect: TestSemantics.fullScreen,
430
          actions: allActions.fold<int>(0, (int previous, SemanticsAction action) => previous | action.index),
431 432 433 434 435 436 437 438
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics));

    // Do the actions work?
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
    int expectedLength = 1;
439
    for (final SemanticsAction action in allActions) {
440 441 442 443 444
      switch (action) {
        case SemanticsAction.moveCursorBackwardByCharacter:
        case SemanticsAction.moveCursorForwardByCharacter:
          semanticsOwner.performAction(expectedId, action, true);
          break;
445
        case SemanticsAction.setSelection:
446
          semanticsOwner.performAction(expectedId, action, <dynamic, dynamic>{
447 448 449 450
            'base': 4,
            'extent': 5,
          });
          break;
451 452 453
        default:
          semanticsOwner.performAction(expectedId, action);
      }
454 455 456 457 458 459 460 461
      expect(performedActions.length, expectedLength);
      expect(performedActions.last, action);
      expectedLength += 1;
    }

    semantics.dispose();
  });

462
  testWidgets('Semantics widget supports all flags', (WidgetTester tester) async {
463
    final SemanticsTester semantics = SemanticsTester(tester);
464
    // Note: checked state and toggled state are mutually exclusive.
465
    await tester.pumpWidget(
466
        Semantics(
467
          key: const Key('a'),
468
          container: true,
469
          explicitChildNodes: true,
470 471
          // flags
          enabled: true,
472
          hidden: true,
473 474 475
          checked: true,
          selected: true,
          button: true,
476
          link: true,
477
          textField: true,
478
          readOnly: true,
479
          focused: true,
480
          focusable: true,
481 482
          inMutuallyExclusiveGroup: true,
          header: true,
483
          obscured: true,
484
          multiline: true,
485 486
          scopesRoute: true,
          namesRoute: true,
487 488
          image: true,
          liveRegion: true,
489
        ),
490
    );
491 492 493
    final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
    flags
      ..remove(SemanticsFlag.hasToggledState)
494 495
      ..remove(SemanticsFlag.isToggled)
      ..remove(SemanticsFlag.hasImplicitScrolling);
496

497
    TestSemantics expectedSemantics = TestSemantics.root(
498
      children: <TestSemantics>[
499
        TestSemantics.rootChild(
500
          rect: TestSemantics.fullScreen,
501
          flags: flags,
502 503 504 505 506
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));

507
    await tester.pumpWidget(Semantics(
508
      key: const Key('b'),
509
      container: true,
510
      scopesRoute: false,
511
    ));
512
    expectedSemantics = TestSemantics.root(
513
      children: <TestSemantics>[
514
        TestSemantics.rootChild(
515 516 517 518 519 520 521
          rect: TestSemantics.fullScreen,
          flags: <SemanticsFlag>[],
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));

522
    await tester.pumpWidget(
523
      Semantics(
524 525 526 527 528
        key: const Key('c'),
        toggled: true,
      ),
    );

529
    expectedSemantics = TestSemantics.root(
530
      children: <TestSemantics>[
531
        TestSemantics.rootChild(
532 533 534 535 536 537 538 539 540 541
          rect: TestSemantics.fullScreen,
          flags: <SemanticsFlag>[
            SemanticsFlag.hasToggledState,
            SemanticsFlag.isToggled,
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
542
    semantics.dispose();
543
  }, skip: isBrowser);
544

545
  testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async {
546
    final SemanticsTester semantics = SemanticsTester(tester);
547
    int semanticsUpdateCount = 0;
548
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
549 550 551 552 553 554 555 556
      listener: () {
        semanticsUpdateCount += 1;
      }
    );

    final List<String> performedActions = <String>[];

    await tester.pumpWidget(
557
      Semantics(
558 559 560 561 562
        container: true,
        onTap: () => performedActions.add('first'),
      ),
    );

563
    const int expectedId = 1;
564
    final TestSemantics expectedSemantics = TestSemantics.root(
565
      children: <TestSemantics>[
566
        TestSemantics.rootChild(
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
          id: expectedId,
          rect: TestSemantics.fullScreen,
          actions: SemanticsAction.tap.index,
        ),
      ],
    );

    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;

    expect(semantics, hasSemantics(expectedSemantics));
    semanticsOwner.performAction(expectedId, SemanticsAction.tap);
    expect(semanticsUpdateCount, 1);
    expect(performedActions, <String>['first']);

    semanticsUpdateCount = 0;
    performedActions.clear();

    // Updating existing handler should not trigger semantics update
    await tester.pumpWidget(
586
      Semantics(
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
        container: true,
        onTap: () => performedActions.add('second'),
      ),
    );

    expect(semantics, hasSemantics(expectedSemantics));
    semanticsOwner.performAction(expectedId, SemanticsAction.tap);
    expect(semanticsUpdateCount, 0);
    expect(performedActions, <String>['second']);

    semanticsUpdateCount = 0;
    performedActions.clear();

    // Adding a handler works
    await tester.pumpWidget(
602
      Semantics(
603 604 605 606 607 608
        container: true,
        onTap: () => performedActions.add('second'),
        onLongPress: () => performedActions.add('longPress'),
      ),
    );

609
    final TestSemantics expectedSemanticsWithLongPress = TestSemantics.root(
610
      children: <TestSemantics>[
611
        TestSemantics.rootChild(
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
          id: expectedId,
          rect: TestSemantics.fullScreen,
          actions: SemanticsAction.tap.index | SemanticsAction.longPress.index,
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemanticsWithLongPress));
    semanticsOwner.performAction(expectedId, SemanticsAction.longPress);
    expect(semanticsUpdateCount, 1);
    expect(performedActions, <String>['longPress']);

    semanticsUpdateCount = 0;
    performedActions.clear();

    // Removing a handler works
    await tester.pumpWidget(
629
      Semantics(
630 631 632 633 634 635 636 637
        container: true,
        onTap: () => performedActions.add('second'),
      ),
    );

    expect(semantics, hasSemantics(expectedSemantics));
    expect(semanticsUpdateCount, 1);

638
    handle.dispose();
639 640
    semantics.dispose();
  });
641

642 643
  testWidgets('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async {
    final SemanticsHandle semantics = tester.ensureSemantics();
644
    await tester.pumpWidget(Semantics(
645
      container: true,
646
      onTap: () { },
647 648 649
      onTapHint: 'test',
    ));

650
    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
651
      hasTapAction: true,
652
      onTapHint: 'test',
653 654
    ));

655
    await tester.pumpWidget(Semantics(
656
      container: true,
657
      onLongPress: () { },
658 659 660
      onLongPressHint: 'foo',
    ));

661
    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
662
      hasLongPressAction: true,
663
      onLongPressHint: 'foo',
664 665 666 667 668 669
    ));
    semantics.dispose();
  });

  testWidgets('CustomSemanticsActions can be added to a Semantics widget', (WidgetTester tester) async {
    final SemanticsHandle semantics = tester.ensureSemantics();
670
    await tester.pumpWidget(Semantics(
671 672
      container: true,
      customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
673 674
        const CustomSemanticsAction(label: 'foo'): () { },
        const CustomSemanticsAction(label: 'bar'): () { },
675 676 677
      },
    ));

678
    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
679 680 681 682 683 684 685 686
      customActions: <CustomSemanticsAction>[
        const CustomSemanticsAction(label: 'bar'),
        const CustomSemanticsAction(label: 'foo'),
      ],
    ));
    semantics.dispose();
  });

687
  testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async {
688
    final SemanticsTester semantics = SemanticsTester(tester);
689 690

    await tester.pumpWidget(
691
      Directionality(
692
        textDirection: TextDirection.ltr,
693
        child: Semantics(
694 695 696 697
          container: true,
          value: '10s',
          increasedValue: '11s',
          decreasedValue: '9s',
698 699
          onIncrease: () => () { },
          onDecrease: () => () { },
700 701 702 703
        ),
      ),
    );

704
    expect(semantics, hasSemantics(TestSemantics.root(
705
      children: <TestSemantics>[
706
        TestSemantics.rootChild(
707 708 709 710 711 712 713 714 715 716 717
          actions: SemanticsAction.increase.index | SemanticsAction.decrease.index,
          textDirection: TextDirection.ltr,
          value: '10s',
          increasedValue: '11s',
          decreasedValue: '9s',
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));

    semantics.dispose();
  });
718 719

  testWidgets('Semantics widgets built in a widget tree are sorted properly', (WidgetTester tester) async {
720
    final SemanticsTester semantics = SemanticsTester(tester);
721
    int semanticsUpdateCount = 0;
722
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
723 724 725 726 727
      listener: () {
        semanticsUpdateCount += 1;
      }
    );
    await tester.pumpWidget(
728
      Directionality(
729
        textDirection: TextDirection.ltr,
730
        child: Semantics(
731 732
          sortKey: const CustomSortKey(0.0),
          explicitChildNodes: true,
733
          child: Column(
734
            children: <Widget>[
735 736 737
              Semantics(sortKey: const CustomSortKey(3.0), child: const Text('Label 1')),
              Semantics(sortKey: const CustomSortKey(2.0), child: const Text('Label 2')),
              Semantics(
738 739
                sortKey: const CustomSortKey(1.0),
                explicitChildNodes: true,
740
                child: Row(
741
                  children: <Widget>[
742 743 744
                    Semantics(sortKey: const OrdinalSortKey(3.0), child: const Text('Label 3')),
                    Semantics(sortKey: const OrdinalSortKey(2.0), child: const Text('Label 4')),
                    Semantics(sortKey: const OrdinalSortKey(1.0), child: const Text('Label 5')),
745 746 747 748 749 750 751 752 753 754
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
755
      TestSemantics.root(
756
        children: <TestSemantics>[
757
          TestSemantics(
758
            id: 1,
759
            children: <TestSemantics>[
760
              TestSemantics(
761
                id: 4,
762
                children: <TestSemantics>[
763
                  TestSemantics(
764 765
                    id: 7,
                    label: r'Label 5',
766 767
                    textDirection: TextDirection.ltr,
                  ),
768
                  TestSemantics(
769
                    id: 6,
770 771 772
                    label: r'Label 4',
                    textDirection: TextDirection.ltr,
                  ),
773
                  TestSemantics(
774 775
                    id: 5,
                    label: r'Label 3',
776 777 778 779
                    textDirection: TextDirection.ltr,
                  ),
                ],
              ),
780
              TestSemantics(
781 782 783 784
                id: 3,
                label: r'Label 2',
                textDirection: TextDirection.ltr,
              ),
785
              TestSemantics(
786 787 788 789
                id: 2,
                label: r'Label 1',
                textDirection: TextDirection.ltr,
              ),
790 791 792
            ],
          ),
        ],
793
      ), ignoreTransform: true, ignoreRect: true),
794
    );
795 796

    handle.dispose();
797 798 799 800
    semantics.dispose();
  });

  testWidgets('Semantics widgets built with explicit sort orders are sorted properly', (WidgetTester tester) async {
801
    final SemanticsTester semantics = SemanticsTester(tester);
802
    int semanticsUpdateCount = 0;
803
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
804 805 806 807 808
      listener: () {
        semanticsUpdateCount += 1;
      }
    );
    await tester.pumpWidget(
809
      Directionality(
810
        textDirection: TextDirection.ltr,
811
        child: Row(
812
          children: <Widget>[
813
            Semantics(
814
              sortKey: const CustomSortKey(3.0),
815 816
              child: const Text('Label 1'),
            ),
817
            Semantics(
818
              sortKey: const CustomSortKey(1.0),
819 820
              child: const Text('Label 2'),
            ),
821
            Semantics(
822 823
              sortKey: const CustomSortKey(2.0),
              child: const Text('Label 3'),
824 825 826 827 828 829 830
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
831
      TestSemantics.root(
832
        children: <TestSemantics>[
833
          TestSemantics(
834
            id: 2,
835 836 837
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
838
          TestSemantics(
839 840
            id: 3,
            label: r'Label 3',
841
            textDirection: TextDirection.ltr,
842
          ),
843
          TestSemantics(
844 845 846
            id: 1,
            label: r'Label 1',
            textDirection: TextDirection.ltr,
847 848
          ),
        ],
849
      ), ignoreTransform: true, ignoreRect: true));
850

851
    handle.dispose();
852 853 854 855
    semantics.dispose();
  });

  testWidgets('Semantics widgets without sort orders are sorted properly', (WidgetTester tester) async {
856
    final SemanticsTester semantics = SemanticsTester(tester);
857
    int semanticsUpdateCount = 0;
858
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
859 860 861 862 863
      listener: () {
        semanticsUpdateCount += 1;
      }
    );
    await tester.pumpWidget(
864
      Directionality(
865
        textDirection: TextDirection.ltr,
866
        child: Column(
867 868 869
          children: <Widget>[
            const Text('Label 1'),
            const Text('Label 2'),
870
            Row(
871
              children: const <Widget>[
872 873 874
                Text('Label 3'),
                Text('Label 4'),
                Text('Label 5'),
875 876 877 878 879 880 881 882
              ],
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
883
      TestSemantics(
884
        children: <TestSemantics>[
885
          TestSemantics(
886 887 888
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
889
          TestSemantics(
890 891 892
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
893
          TestSemantics(
894 895 896
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
897
          TestSemantics(
898 899 900
            label: r'Label 4',
            textDirection: TextDirection.ltr,
          ),
901
          TestSemantics(
902 903 904 905 906 907
            label: r'Label 5',
            textDirection: TextDirection.ltr,
          ),
        ],
      ), ignoreTransform: true, ignoreRect: true, ignoreId: true),
    );
908 909

    handle.dispose();
910 911 912 913
    semantics.dispose();
  });

  testWidgets('Semantics widgets that are transformed are sorted properly', (WidgetTester tester) async {
914
    final SemanticsTester semantics = SemanticsTester(tester);
915
    int semanticsUpdateCount = 0;
916
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
917 918 919 920 921
      listener: () {
        semanticsUpdateCount += 1;
      }
    );
    await tester.pumpWidget(
922
      Directionality(
923
        textDirection: TextDirection.ltr,
924
        child: Column(
925 926 927
          children: <Widget>[
            const Text('Label 1'),
            const Text('Label 2'),
928
            Transform.rotate(
929
              angle: pi / 2.0,
930
              child: Row(
931
                children: const <Widget>[
932 933 934
                  Text('Label 3'),
                  Text('Label 4'),
                  Text('Label 5'),
935 936 937 938 939 940 941 942 943
                ],
              ),
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
944
      TestSemantics(
945
        children: <TestSemantics>[
946
          TestSemantics(
947 948 949
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
950
          TestSemantics(
951 952 953
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
954
          TestSemantics(
955 956 957
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
958
          TestSemantics(
959 960 961
            label: r'Label 4',
            textDirection: TextDirection.ltr,
          ),
962
          TestSemantics(
963 964 965 966 967 968
            label: r'Label 5',
            textDirection: TextDirection.ltr,
          ),
        ],
      ), ignoreTransform: true, ignoreRect: true, ignoreId: true),
    );
969 970

    handle.dispose();
971 972 973
    semantics.dispose();
  });

974
  testWidgets('Semantics widgets without sort orders are sorted properly when no Directionality is present', (WidgetTester tester) async {
975
    final SemanticsTester semantics = SemanticsTester(tester);
976
    int semanticsUpdateCount = 0;
977
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(listener: () {
978 979 980
      semanticsUpdateCount += 1;
    });
    await tester.pumpWidget(
981
      Stack(
982 983 984 985 986 987 988 989 990 991
        alignment: Alignment.center,
        children: <Widget>[
          // Set this up so that the placeholder takes up the whole screen,
          // and place the positioned boxes so that if we traverse in the
          // geometric order, we would go from box [4, 3, 2, 1, 0], but if we
          // go in child order, then we go from box [4, 1, 2, 3, 0]. We're verifying
          // that we go in child order here, not geometric order, since there
          // is no directionality, so we don't have a geometric opinion about
          // horizontal order. We do still want to sort vertically, however,
          // which is why the order isn't [0, 1, 2, 3, 4].
992
          Semantics(
993 994 995
            button: true,
            child: const Placeholder(),
          ),
996
          Positioned(
997 998
            top: 200.0,
            left: 100.0,
999
            child: Semantics( // Box 0
1000 1001 1002 1003
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1004
          Positioned(
1005 1006
            top: 100.0,
            left: 200.0,
1007
            child: Semantics( // Box 1
1008 1009 1010 1011
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1012
          Positioned(
1013 1014
            top: 100.0,
            left: 100.0,
1015
            child: Semantics( // Box 2
1016 1017 1018 1019
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1020
          Positioned(
1021 1022
            top: 100.0,
            left: 0.0,
1023
            child: Semantics( // Box 3
1024 1025 1026 1027
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1028
          Positioned(
1029 1030
            top: 10.0,
            left: 100.0,
1031
            child: Semantics( // Box 4
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
        ],
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(
      semantics,
      hasSemantics(
1043
        TestSemantics(
1044
          children: <TestSemantics>[
1045
            TestSemantics(
1046 1047
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1048
            TestSemantics(
1049 1050
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1051
            TestSemantics(
1052 1053
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1054
            TestSemantics(
1055 1056
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1057
            TestSemantics(
1058 1059
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1060
            TestSemantics(
1061 1062 1063 1064 1065 1066 1067 1068
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
          ],
        ),
        ignoreTransform: true,
        ignoreRect: true,
        ignoreId: true),
    );
1069 1070

    handle.dispose();
1071 1072
    semantics.dispose();
  });
1073 1074

  testWidgets('Semantics excludeSemantics ignores children', (WidgetTester tester) async {
1075 1076
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(Semantics(
1077 1078 1079
      label: 'label',
      excludeSemantics: true,
      textDirection: TextDirection.ltr,
1080
      child: Semantics(
1081 1082 1083 1084 1085 1086
        label: 'other label',
        textDirection: TextDirection.ltr,
      ),
    ));

    expect(semantics, hasSemantics(
1087
      TestSemantics(
1088
        children: <TestSemantics>[
1089
          TestSemantics(
1090 1091 1092 1093
            label: 'label',
            textDirection: TextDirection.ltr,
          ),
        ],
1094
      ), ignoreId: true, ignoreRect: true, ignoreTransform: true),
1095 1096 1097
    );
    semantics.dispose();
  });
1098 1099 1100 1101
}

class CustomSortKey extends OrdinalSortKey {
  const CustomSortKey(double order, {String name}) : super(order, name: name);
1102
}