semantics_test.dart 32.2 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 345
              value: 'value two',
              textDirection: TextDirection.ltr,
            ),
          ]
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 439
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics));

    // Do the actions work?
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
    int expectedLength = 1;
    for (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 476
          checked: true,
          selected: true,
          button: true,
          textField: true,
477
          readOnly: true,
478 479 480
          focused: true,
          inMutuallyExclusiveGroup: true,
          header: true,
481
          obscured: true,
482
          multiline: true,
483 484
          scopesRoute: true,
          namesRoute: true,
485 486
          image: true,
          liveRegion: true,
487 488
        )
    );
489 490 491
    final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
    flags
      ..remove(SemanticsFlag.hasToggledState)
492 493
      ..remove(SemanticsFlag.isToggled)
      ..remove(SemanticsFlag.hasImplicitScrolling);
494

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

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

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

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

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

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

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

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

561
    const int expectedId = 1;
562
    final TestSemantics expectedSemantics = TestSemantics.root(
563
      children: <TestSemantics>[
564
        TestSemantics.rootChild(
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
          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(
584
      Semantics(
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
        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(
600
      Semantics(
601 602 603 604 605 606
        container: true,
        onTap: () => performedActions.add('second'),
        onLongPress: () => performedActions.add('longPress'),
      ),
    );

607
    final TestSemantics expectedSemanticsWithLongPress = TestSemantics.root(
608
      children: <TestSemantics>[
609
        TestSemantics.rootChild(
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
          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(
627
      Semantics(
628 629 630 631 632 633 634 635
        container: true,
        onTap: () => performedActions.add('second'),
      ),
    );

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

636
    handle.dispose();
637 638
    semantics.dispose();
  });
639

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

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

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

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

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

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

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

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

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

    semantics.dispose();
  });
716 717

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

    handle.dispose();
795 796 797 798
    semantics.dispose();
  });

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

849
    handle.dispose();
850 851 852 853
    semantics.dispose();
  });

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

    handle.dispose();
908 909 910 911
    semantics.dispose();
  });

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

    handle.dispose();
969 970 971
    semantics.dispose();
  });

972
  testWidgets('Semantics widgets without sort orders are sorted properly when no Directionality is present', (WidgetTester tester) async {
973
    final SemanticsTester semantics = SemanticsTester(tester);
974
    int semanticsUpdateCount = 0;
975
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(listener: () {
976 977 978
      semanticsUpdateCount += 1;
    });
    await tester.pumpWidget(
979
      Stack(
980 981 982 983 984 985 986 987 988 989
        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].
990
          Semantics(
991 992 993
            button: true,
            child: const Placeholder(),
          ),
994
          Positioned(
995 996
            top: 200.0,
            left: 100.0,
997
            child: Semantics( // Box 0
998 999 1000 1001
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1002
          Positioned(
1003 1004
            top: 100.0,
            left: 200.0,
1005
            child: Semantics( // Box 1
1006 1007 1008 1009
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1010
          Positioned(
1011 1012
            top: 100.0,
            left: 100.0,
1013
            child: Semantics( // Box 2
1014 1015 1016 1017
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1018
          Positioned(
1019 1020
            top: 100.0,
            left: 0.0,
1021
            child: Semantics( // Box 3
1022 1023 1024 1025
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1026
          Positioned(
1027 1028
            top: 10.0,
            left: 100.0,
1029
            child: Semantics( // Box 4
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
        ],
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(
      semantics,
      hasSemantics(
1041
        TestSemantics(
1042
          children: <TestSemantics>[
1043
            TestSemantics(
1044 1045
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1046
            TestSemantics(
1047 1048
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1049
            TestSemantics(
1050 1051
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1052
            TestSemantics(
1053 1054
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1055
            TestSemantics(
1056 1057
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1058
            TestSemantics(
1059 1060 1061 1062 1063 1064 1065 1066
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
          ],
        ),
        ignoreTransform: true,
        ignoreRect: true,
        ignoreId: true),
    );
1067 1068

    handle.dispose();
1069 1070
    semantics.dispose();
  });
1071 1072

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

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

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