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

7
import 'package:flutter/material.dart';
8
import 'package:flutter/rendering.dart';
9 10
import 'package:flutter_test/flutter_test.dart';

11
import 'semantics_tester.dart';
12 13

void main() {
14 15 16 17
  setUp(() {
    debugResetSemanticsIdCounter();
  });

18
  testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
19
    SemanticsTester? semantics = SemanticsTester(tester);
20

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

    await tester.pumpWidget(
31 32 33 34
      Semantics(
        label: 'test1',
        textDirection: TextDirection.ltr,
        child: Container(),
35
      ),
36 37
    );

38 39 40 41 42 43
    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
44

45 46
    semantics.dispose();
    semantics = null;
47 48

    expect(tester.binding.hasScheduledFrame, isFalse);
49
    semantics = SemanticsTester(tester);
50 51 52
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();

53 54 55 56 57 58
    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
59
    semantics.dispose();
60
  }, semanticsEnabled: false);
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
  testWidgets('Semantics tag only applies to immediate child', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
        Directionality(
            textDirection: TextDirection.ltr,
            child: ListView(
              children: <Widget>[
                SingleChildScrollView(
                  child: Column(
                    children: <Widget>[
                      Container(padding: const EdgeInsets.only(top: 20.0)),
                      const Text('label'),
                    ],
                  ),
                ),
              ],
            ),
        ),
    );

    expect(semantics, isNot(includesNodeWith(
      flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
      tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics},
    )));

    await tester.pump();
    // Semantics should stay the same after a frame update.
    expect(semantics, isNot(includesNodeWith(
      flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
      tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics},
    )));

    semantics.dispose();
  }, semanticsEnabled: false);

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
  testWidgets('Semantics tooltip', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          tooltip: 'test1',
          textDirection: TextDirection.ltr,
        ),
      ],
    );

    await tester.pumpWidget(
      Semantics(
        tooltip: 'test1',
        textDirection: TextDirection.ltr,
      ),
    );

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

126
  testWidgets('Detach and reattach assert', (WidgetTester tester) async {
127 128
    final SemanticsTester semantics = SemanticsTester(tester);
    final GlobalKey key = GlobalKey();
129

130
    await tester.pumpWidget(Directionality(
Ian Hickson's avatar
Ian Hickson committed
131
      textDirection: TextDirection.ltr,
132 133
      child: Semantics(
        label: 'test1',
134
        child: Semantics(
135 136 137 138
          key: key,
          container: true,
          label: 'test2a',
          child: Container(),
139 140
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
141
    ));
142

143
    expect(semantics, hasSemantics(
144
      TestSemantics.root(
145
        children: <TestSemantics>[
146
          TestSemantics.rootChild(
147 148
            label: 'test1',
            children: <TestSemantics>[
149
              TestSemantics(
150
                label: 'test2a',
151 152 153
              ),
            ],
          ),
154
        ],
155 156 157 158
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
159
    ));
160

161
    await tester.pumpWidget(Directionality(
Ian Hickson's avatar
Ian Hickson committed
162
      textDirection: TextDirection.ltr,
163 164
      child: Semantics(
        label: 'test1',
165
        child: Semantics(
166 167
          container: true,
          label: 'middle',
168
          child: Semantics(
169
            key: key,
170
            container: true,
171 172
            label: 'test2b',
            child: Container(),
173 174 175
          ),
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
176
    ));
177

178
    expect(semantics, hasSemantics(
179
      TestSemantics.root(
180
        children: <TestSemantics>[
181
          TestSemantics.rootChild(
182 183
            label: 'test1',
            children: <TestSemantics>[
184
              TestSemantics(
185
                label: 'middle',
186
                children: <TestSemantics>[
187
                  TestSemantics(
188 189 190
                    label: 'test2b',
                  ),
                ],
191 192 193
              ),
            ],
          ),
194
        ],
195 196 197 198
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
199
    ));
200

201
    semantics.dispose();
202
  });
Ian Hickson's avatar
Ian Hickson committed
203 204

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

    await tester.pumpWidget(
208
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
209
        textDirection: TextDirection.rtl,
210
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
211
          label: 'test1',
212
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
213 214 215 216
        ),
      ),
    );

217
    expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl));
218
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
219 220 221
  });

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

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

234
    expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr));
235
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
236 237
  });

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

241
    final TestSemantics expectedSemantics = TestSemantics.root(
242
      children: <TestSemantics>[
243
        TestSemantics.rootChild(
244 245
          label: 'test1',
          textDirection: TextDirection.ltr,
246
        ),
247
      ],
Ian Hickson's avatar
Ian Hickson committed
248 249 250
    );

    await tester.pumpWidget(
251
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
252
        textDirection: TextDirection.rtl,
253
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
254 255
          label: 'test1',
          textDirection: TextDirection.ltr,
256
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
257 258 259 260
        ),
      ),
    );

261
    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
262
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
263 264
  });

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

268
    final TestSemantics expectedSemantics = TestSemantics.root(
269
      children: <TestSemantics>[
270
        TestSemantics.rootChild(
271 272
          label: 'test1',
          textDirection: TextDirection.rtl,
273
        ),
274
      ],
Ian Hickson's avatar
Ian Hickson committed
275 276 277
    );

    await tester.pumpWidget(
278
      Directionality(
Ian Hickson's avatar
Ian Hickson committed
279
        textDirection: TextDirection.ltr,
280
        child: Semantics(
Ian Hickson's avatar
Ian Hickson committed
281 282
          label: 'test1',
          textDirection: TextDirection.rtl,
283
          child: Container(),
Ian Hickson's avatar
Ian Hickson committed
284 285 286 287
        ),
      ),
    );

288
    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
289
    semantics.dispose();
Ian Hickson's avatar
Ian Hickson committed
290
  });
291 292

  testWidgets('Semantics label and hint', (WidgetTester tester) async {
293
    final SemanticsTester semantics = SemanticsTester(tester);
294 295

    await tester.pumpWidget(
296
      Directionality(
297
        textDirection: TextDirection.ltr,
298
        child: Semantics(
299 300 301
          label: 'label',
          hint: 'hint',
          value: 'value',
302
          child: Container(),
303 304 305 306
        ),
      ),
    );

307
    final TestSemantics expectedSemantics = TestSemantics.root(
308
      children: <TestSemantics>[
309
        TestSemantics.rootChild(
310 311 312 313
          label: 'label',
          hint: 'hint',
          value: 'value',
          textDirection: TextDirection.ltr,
314
        ),
315
      ],
316 317 318
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
319
    semantics.dispose();
320 321 322
  });

  testWidgets('Semantics hints can merge', (WidgetTester tester) async {
323
    final SemanticsTester semantics = SemanticsTester(tester);
324 325

    await tester.pumpWidget(
326
      Directionality(
327
        textDirection: TextDirection.ltr,
328
        child: Semantics(
329
          container: true,
330
          child: Column(
331
            children: <Widget>[
332
              Semantics(
333 334
                hint: 'hint one',
              ),
335
              Semantics(
336
                hint: 'hint two',
337
              ),
338 339 340 341 342 343 344

            ],
          ),
        ),
      ),
    );

345
    final TestSemantics expectedSemantics = TestSemantics.root(
346
      children: <TestSemantics>[
347
        TestSemantics.rootChild(
348 349
          hint: 'hint one\nhint two',
          textDirection: TextDirection.ltr,
350
        ),
351
      ],
352 353 354
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
355
    semantics.dispose();
356 357 358
  });

  testWidgets('Semantics values do not merge', (WidgetTester tester) async {
359
    final SemanticsTester semantics = SemanticsTester(tester);
360 361

    await tester.pumpWidget(
362
      Directionality(
363
        textDirection: TextDirection.ltr,
364
        child: Semantics(
365
          container: true,
366
          child: Column(
367
            children: <Widget>[
368
              Semantics(
369
                value: 'value one',
370
                child: const SizedBox(
371 372
                  height: 10.0,
                  width: 10.0,
373
                ),
374
              ),
375
              Semantics(
376
                value: 'value two',
377
                child: const SizedBox(
378 379
                  height: 10.0,
                  width: 10.0,
380 381
                ),
              ),
382 383 384 385 386 387
            ],
          ),
        ),
      ),
    );

388
    final TestSemantics expectedSemantics = TestSemantics.root(
389
      children: <TestSemantics>[
390
        TestSemantics.rootChild(
391
          children: <TestSemantics>[
392
            TestSemantics(
393 394 395
              value: 'value one',
              textDirection: TextDirection.ltr,
            ),
396
            TestSemantics(
397 398 399
              value: 'value two',
              textDirection: TextDirection.ltr,
            ),
400
          ],
401
        ),
402 403 404 405
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
406
    semantics.dispose();
407 408 409
  });

  testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
410
    final SemanticsTester semantics = SemanticsTester(tester);
411 412

    await tester.pumpWidget(
413
      Directionality(
414
        textDirection: TextDirection.ltr,
415
        child: Semantics(
416
          container: true,
417
          child: Column(
418
            children: <Widget>[
419
              Semantics(
420 421
                hint: 'hint',
              ),
422
              Semantics(
423 424 425 426 427 428 429 430
                value: 'value',
              ),
            ],
          ),
        ),
      ),
    );

431
    final TestSemantics expectedSemantics = TestSemantics.root(
432
      children: <TestSemantics>[
433
        TestSemantics.rootChild(
434 435 436
          hint: 'hint',
          value: 'value',
          textDirection: TextDirection.ltr,
437
        ),
438
      ],
439 440 441
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
442
    semantics.dispose();
443
  });
444

445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
    testWidgets('Semantics tagForChildren works', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          container: true,
          tagForChildren: const SemanticsTag('custom tag'),
          child: Column(
            children: <Widget>[
              Semantics(
                container: true,
                child: const Text('child 1'),
              ),
              Semantics(
                container: true,
                child: const Text('child 2'),
              ),
            ],
          ),
        ),
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: 'child 1',
              tags: <SemanticsTag>[const SemanticsTag('custom tag')],
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'child 2',
              tags: <SemanticsTag>[const SemanticsTag('custom tag')],
              textDirection: TextDirection.ltr,
            ),
484
          ],
485 486 487 488 489 490 491 492
        ),
      ],
    );

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

493
  testWidgets('Semantics widget supports all actions', (WidgetTester tester) async {
494
    final SemanticsTester semantics = SemanticsTester(tester);
495 496 497 498

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

    await tester.pumpWidget(
499
      Semantics(
500
        container: true,
501
        onDismiss: () => performedActions.add(SemanticsAction.dismiss),
502 503 504 505 506 507 508 509
        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),
510 511 512
        onCopy: () => performedActions.add(SemanticsAction.copy),
        onCut: () => performedActions.add(SemanticsAction.cut),
        onPaste: () => performedActions.add(SemanticsAction.paste),
513 514
        onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
        onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
515
        onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
516
        onSetText: (String _) => performedActions.add(SemanticsAction.setText),
517 518
        onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
        onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
519
      ),
520 521
    );

522
    final Set<SemanticsAction> allActions = SemanticsAction.values.toSet()
523 524
      ..remove(SemanticsAction.moveCursorForwardByWord)
      ..remove(SemanticsAction.moveCursorBackwardByWord)
525
      ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
526
      ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
527

528
    const int expectedId = 1;
529
    final TestSemantics expectedSemantics = TestSemantics.root(
530
      children: <TestSemantics>[
531
        TestSemantics.rootChild(
532 533
          id: expectedId,
          rect: TestSemantics.fullScreen,
534
          actions: allActions.fold<int>(0, (int previous, SemanticsAction action) => previous | action.index),
535 536 537 538 539 540
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics));

    // Do the actions work?
541
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
542
    int expectedLength = 1;
543
    for (final SemanticsAction action in allActions) {
544 545 546 547
      switch (action) {
        case SemanticsAction.moveCursorBackwardByCharacter:
        case SemanticsAction.moveCursorForwardByCharacter:
          semanticsOwner.performAction(expectedId, action, true);
548
        case SemanticsAction.setSelection:
549
          semanticsOwner.performAction(expectedId, action, <dynamic, dynamic>{
550 551 552
            'base': 4,
            'extent': 5,
          });
553 554
        case SemanticsAction.setText:
          semanticsOwner.performAction(expectedId, action, 'text');
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
        case SemanticsAction.copy:
        case SemanticsAction.customAction:
        case SemanticsAction.cut:
        case SemanticsAction.decrease:
        case SemanticsAction.didGainAccessibilityFocus:
        case SemanticsAction.didLoseAccessibilityFocus:
        case SemanticsAction.dismiss:
        case SemanticsAction.increase:
        case SemanticsAction.longPress:
        case SemanticsAction.moveCursorBackwardByWord:
        case SemanticsAction.moveCursorForwardByWord:
        case SemanticsAction.paste:
        case SemanticsAction.scrollDown:
        case SemanticsAction.scrollLeft:
        case SemanticsAction.scrollRight:
        case SemanticsAction.scrollUp:
        case SemanticsAction.showOnScreen:
        case SemanticsAction.tap:
573 574
          semanticsOwner.performAction(expectedId, action);
      }
575 576 577 578 579 580
      expect(performedActions.length, expectedLength);
      expect(performedActions.last, action);
      expectedLength += 1;
    }

    semantics.dispose();
581
  });
582

583
  testWidgets('Semantics widget supports all flags', (WidgetTester tester) async {
584
    final SemanticsTester semantics = SemanticsTester(tester);
585
    // Checked state and toggled state are mutually exclusive.
586
    await tester.pumpWidget(
587
        Semantics(
588
          key: const Key('a'),
589
          container: true,
590
          explicitChildNodes: true,
591 592
          // flags
          enabled: true,
593
          hidden: true,
594 595 596
          checked: true,
          selected: true,
          button: true,
597
          slider: true,
598
          keyboardKey: true,
599
          link: true,
600
          textField: true,
601
          readOnly: true,
602
          focused: true,
603
          focusable: true,
604 605
          inMutuallyExclusiveGroup: true,
          header: true,
606
          obscured: true,
607
          multiline: true,
608 609
          scopesRoute: true,
          namesRoute: true,
610 611
          image: true,
          liveRegion: true,
612
        ),
613
    );
614
    final List<SemanticsFlag> flags = SemanticsFlag.values.toList();
615 616
    flags
      ..remove(SemanticsFlag.hasToggledState)
617
      ..remove(SemanticsFlag.isToggled)
618 619
      ..remove(SemanticsFlag.hasImplicitScrolling)
      ..remove(SemanticsFlag.isCheckStateMixed);
620

621
    TestSemantics expectedSemantics = TestSemantics.root(
622
      children: <TestSemantics>[
623
        TestSemantics.rootChild(
624
          rect: TestSemantics.fullScreen,
625
          flags: flags,
626 627 628 629 630
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));

631
    await tester.pumpWidget(Semantics(
632
      key: const Key('b'),
633
      container: true,
634
      scopesRoute: false,
635
    ));
636
    expectedSemantics = TestSemantics.root(
637
      children: <TestSemantics>[
638
        TestSemantics.rootChild(
639 640 641 642 643 644 645
          rect: TestSemantics.fullScreen,
          flags: <SemanticsFlag>[],
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));

646
    await tester.pumpWidget(
647
      Semantics(
648 649 650 651 652
        key: const Key('c'),
        toggled: true,
      ),
    );

653
    expectedSemantics = TestSemantics.root(
654
      children: <TestSemantics>[
655
        TestSemantics.rootChild(
656 657 658 659 660 661 662 663 664 665
          rect: TestSemantics.fullScreen,
          flags: <SemanticsFlag>[
            SemanticsFlag.hasToggledState,
            SemanticsFlag.isToggled,
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698

    await tester.pumpWidget(
        Semantics(
          key: const Key('a'),
          container: true,
          explicitChildNodes: true,
          // flags
          enabled: true,
          hidden: true,
          checked: false,
          mixed: true,
          selected: true,
          button: true,
          slider: true,
          keyboardKey: true,
          link: true,
          textField: true,
          readOnly: true,
          focused: true,
          focusable: true,
          inMutuallyExclusiveGroup: true,
          header: true,
          obscured: true,
          multiline: true,
          scopesRoute: true,
          namesRoute: true,
          image: true,
          liveRegion: true,
        ),
    );
    flags
      ..remove(SemanticsFlag.isChecked)
      ..add(SemanticsFlag.isCheckStateMixed);
699
    semantics.dispose();
700 701 702 703 704 705 706 707 708 709
    expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          rect: TestSemantics.fullScreen,
          flags: flags,
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
  });
710

711
  testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async {
712
    final SemanticsTester semantics = SemanticsTester(tester);
713
    int semanticsUpdateCount = 0;
714
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
715 716
      listener: () {
        semanticsUpdateCount += 1;
717
      },
718 719 720 721 722
    );

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

    await tester.pumpWidget(
723
      Semantics(
724 725 726 727 728
        container: true,
        onTap: () => performedActions.add('first'),
      ),
    );

729
    const int expectedId = 1;
730
    final TestSemantics expectedSemantics = TestSemantics.root(
731
      children: <TestSemantics>[
732
        TestSemantics.rootChild(
733 734 735 736 737 738 739
          id: expectedId,
          rect: TestSemantics.fullScreen,
          actions: SemanticsAction.tap.index,
        ),
      ],
    );

740
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
741 742 743 744 745 746 747 748 749 750 751

    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(
752
      Semantics(
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
        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(
768
      Semantics(
769 770 771 772 773 774
        container: true,
        onTap: () => performedActions.add('second'),
        onLongPress: () => performedActions.add('longPress'),
      ),
    );

775
    final TestSemantics expectedSemanticsWithLongPress = TestSemantics.root(
776
      children: <TestSemantics>[
777
        TestSemantics.rootChild(
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
          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(
795
      Semantics(
796 797 798 799 800 801 802 803
        container: true,
        onTap: () => performedActions.add('second'),
      ),
    );

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

804
    handle.dispose();
805 806
    semantics.dispose();
  });
807

808 809
  testWidgets('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async {
    final SemanticsHandle semantics = tester.ensureSemantics();
810
    await tester.pumpWidget(Semantics(
811
      container: true,
812
      onTap: () { },
813 814 815
      onTapHint: 'test',
    ));

816
    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
817
      hasTapAction: true,
818
      onTapHint: 'test',
819 820
    ));

821
    await tester.pumpWidget(Semantics(
822
      container: true,
823
      onLongPress: () { },
824 825 826
      onLongPressHint: 'foo',
    ));

827
    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
828
      hasLongPressAction: true,
829
      onLongPressHint: 'foo',
830 831 832 833 834 835
    ));
    semantics.dispose();
  });

  testWidgets('CustomSemanticsActions can be added to a Semantics widget', (WidgetTester tester) async {
    final SemanticsHandle semantics = tester.ensureSemantics();
836
    await tester.pumpWidget(Semantics(
837 838
      container: true,
      customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
839 840
        const CustomSemanticsAction(label: 'foo'): () { },
        const CustomSemanticsAction(label: 'bar'): () { },
841 842 843
      },
    ));

844
    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
845 846 847 848 849 850 851 852
      customActions: <CustomSemanticsAction>[
        const CustomSemanticsAction(label: 'bar'),
        const CustomSemanticsAction(label: 'foo'),
      ],
    ));
    semantics.dispose();
  });

853
  testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async {
854
    final SemanticsTester semantics = SemanticsTester(tester);
855 856

    await tester.pumpWidget(
857
      Directionality(
858
        textDirection: TextDirection.ltr,
859
        child: Semantics(
860 861 862 863
          container: true,
          value: '10s',
          increasedValue: '11s',
          decreasedValue: '9s',
864 865
          onIncrease: () => () { },
          onDecrease: () => () { },
866 867 868 869
        ),
      ),
    );

870
    expect(semantics, hasSemantics(TestSemantics.root(
871
      children: <TestSemantics>[
872
        TestSemantics.rootChild(
873 874 875 876 877 878 879 880 881 882 883
          actions: SemanticsAction.increase.index | SemanticsAction.decrease.index,
          textDirection: TextDirection.ltr,
          value: '10s',
          increasedValue: '11s',
          decreasedValue: '9s',
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));

    semantics.dispose();
  });
884 885

  testWidgets('Semantics widgets built in a widget tree are sorted properly', (WidgetTester tester) async {
886
    final SemanticsTester semantics = SemanticsTester(tester);
887
    int semanticsUpdateCount = 0;
888
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
889 890
      listener: () {
        semanticsUpdateCount += 1;
891
      },
892 893
    );
    await tester.pumpWidget(
894
      Directionality(
895
        textDirection: TextDirection.ltr,
896
        child: Semantics(
897 898
          sortKey: const CustomSortKey(0.0),
          explicitChildNodes: true,
899
          child: Column(
900
            children: <Widget>[
901 902 903
              Semantics(sortKey: const CustomSortKey(3.0), child: const Text('Label 1')),
              Semantics(sortKey: const CustomSortKey(2.0), child: const Text('Label 2')),
              Semantics(
904 905
                sortKey: const CustomSortKey(1.0),
                explicitChildNodes: true,
906
                child: Row(
907
                  children: <Widget>[
908 909 910
                    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')),
911 912 913 914 915 916 917 918 919 920
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
921
      TestSemantics.root(
922
        children: <TestSemantics>[
923
          TestSemantics(
924
            id: 1,
925
            children: <TestSemantics>[
926
              TestSemantics(
927
                id: 4,
928
                children: <TestSemantics>[
929
                  TestSemantics(
930 931
                    id: 7,
                    label: r'Label 5',
932 933
                    textDirection: TextDirection.ltr,
                  ),
934
                  TestSemantics(
935
                    id: 6,
936 937 938
                    label: r'Label 4',
                    textDirection: TextDirection.ltr,
                  ),
939
                  TestSemantics(
940 941
                    id: 5,
                    label: r'Label 3',
942 943 944 945
                    textDirection: TextDirection.ltr,
                  ),
                ],
              ),
946
              TestSemantics(
947 948 949 950
                id: 3,
                label: r'Label 2',
                textDirection: TextDirection.ltr,
              ),
951
              TestSemantics(
952 953 954 955
                id: 2,
                label: r'Label 1',
                textDirection: TextDirection.ltr,
              ),
956 957 958
            ],
          ),
        ],
959 960 961 962
      ),
      ignoreTransform: true,
      ignoreRect: true,
    ));
963 964

    handle.dispose();
965 966 967 968
    semantics.dispose();
  });

  testWidgets('Semantics widgets built with explicit sort orders are sorted properly', (WidgetTester tester) async {
969
    final SemanticsTester semantics = SemanticsTester(tester);
970
    int semanticsUpdateCount = 0;
971
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
972 973
      listener: () {
        semanticsUpdateCount += 1;
974
      },
975 976
    );
    await tester.pumpWidget(
977
      Directionality(
978
        textDirection: TextDirection.ltr,
979
        child: Row(
980
          children: <Widget>[
981
            Semantics(
982
              sortKey: const CustomSortKey(3.0),
983 984
              child: const Text('Label 1'),
            ),
985
            Semantics(
986
              sortKey: const CustomSortKey(1.0),
987 988
              child: const Text('Label 2'),
            ),
989
            Semantics(
990 991
              sortKey: const CustomSortKey(2.0),
              child: const Text('Label 3'),
992 993 994 995 996 997 998
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
999
      TestSemantics.root(
1000
        children: <TestSemantics>[
1001
          TestSemantics(
1002
            id: 2,
1003 1004 1005
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
1006
          TestSemantics(
1007 1008
            id: 3,
            label: r'Label 3',
1009
            textDirection: TextDirection.ltr,
1010
          ),
1011
          TestSemantics(
1012 1013 1014
            id: 1,
            label: r'Label 1',
            textDirection: TextDirection.ltr,
1015 1016
          ),
        ],
1017 1018 1019 1020
      ),
      ignoreTransform: true,
      ignoreRect: true,
    ));
1021

1022
    handle.dispose();
1023 1024 1025 1026
    semantics.dispose();
  });

  testWidgets('Semantics widgets without sort orders are sorted properly', (WidgetTester tester) async {
1027
    final SemanticsTester semantics = SemanticsTester(tester);
1028
    int semanticsUpdateCount = 0;
1029
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
1030 1031
      listener: () {
        semanticsUpdateCount += 1;
1032
      },
1033 1034
    );
    await tester.pumpWidget(
1035
      const Directionality(
1036
        textDirection: TextDirection.ltr,
1037
        child: Column(
1038
          children: <Widget>[
1039 1040
            Text('Label 1'),
            Text('Label 2'),
1041
            Row(
1042
              children: <Widget>[
1043 1044 1045
                Text('Label 3'),
                Text('Label 4'),
                Text('Label 5'),
1046 1047 1048 1049 1050 1051 1052 1053
              ],
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
1054
      TestSemantics(
1055
        children: <TestSemantics>[
1056
          TestSemantics(
1057 1058 1059
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
1060
          TestSemantics(
1061 1062 1063
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
1064
          TestSemantics(
1065 1066 1067
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
1068
          TestSemantics(
1069 1070 1071
            label: r'Label 4',
            textDirection: TextDirection.ltr,
          ),
1072
          TestSemantics(
1073 1074 1075 1076
            label: r'Label 5',
            textDirection: TextDirection.ltr,
          ),
        ],
1077 1078 1079 1080 1081
      ),
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
1082 1083

    handle.dispose();
1084 1085 1086 1087
    semantics.dispose();
  });

  testWidgets('Semantics widgets that are transformed are sorted properly', (WidgetTester tester) async {
1088
    final SemanticsTester semantics = SemanticsTester(tester);
1089
    int semanticsUpdateCount = 0;
1090
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
1091 1092
      listener: () {
        semanticsUpdateCount += 1;
1093
      },
1094 1095
    );
    await tester.pumpWidget(
1096
      Directionality(
1097
        textDirection: TextDirection.ltr,
1098
        child: Column(
1099 1100 1101
          children: <Widget>[
            const Text('Label 1'),
            const Text('Label 2'),
1102
            Transform.rotate(
1103
              angle: pi / 2.0,
1104 1105
              child: const Row(
                children: <Widget>[
1106 1107 1108
                  Text('Label 3'),
                  Text('Label 4'),
                  Text('Label 5'),
1109 1110 1111 1112 1113 1114 1115 1116 1117
                ],
              ),
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
1118
      TestSemantics(
1119
        children: <TestSemantics>[
1120
          TestSemantics(
1121 1122 1123
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
1124
          TestSemantics(
1125 1126 1127
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
1128
          TestSemantics(
1129 1130 1131
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
1132
          TestSemantics(
1133 1134 1135
            label: r'Label 4',
            textDirection: TextDirection.ltr,
          ),
1136
          TestSemantics(
1137 1138 1139 1140
            label: r'Label 5',
            textDirection: TextDirection.ltr,
          ),
        ],
1141 1142 1143 1144 1145
      ),
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
1146 1147

    handle.dispose();
1148 1149 1150
    semantics.dispose();
  });

1151
  testWidgets('Semantics widgets without sort orders are sorted properly when no Directionality is present', (WidgetTester tester) async {
1152
    final SemanticsTester semantics = SemanticsTester(tester);
1153
    int semanticsUpdateCount = 0;
1154
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(listener: () {
1155 1156 1157
      semanticsUpdateCount += 1;
    });
    await tester.pumpWidget(
1158
      Stack(
1159 1160 1161 1162 1163 1164 1165 1166 1167 1168
        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].
1169
          Semantics(
1170 1171 1172
            button: true,
            child: const Placeholder(),
          ),
1173
          Positioned(
1174 1175
            top: 200.0,
            left: 100.0,
1176
            child: Semantics( // Box 0
1177 1178 1179 1180
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1181
          Positioned(
1182 1183
            top: 100.0,
            left: 200.0,
1184
            child: Semantics( // Box 1
1185 1186 1187 1188
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1189
          Positioned(
1190 1191
            top: 100.0,
            left: 100.0,
1192
            child: Semantics( // Box 2
1193 1194 1195 1196
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1197
          Positioned(
1198 1199
            top: 100.0,
            left: 0.0,
1200
            child: Semantics( // Box 3
1201 1202 1203 1204
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
1205
          Positioned(
1206 1207
            top: 10.0,
            left: 100.0,
1208
            child: Semantics( // Box 4
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
        ],
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(
      semantics,
      hasSemantics(
1220
        TestSemantics(
1221
          children: <TestSemantics>[
1222
            TestSemantics(
1223 1224
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1225
            TestSemantics(
1226 1227
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1228
            TestSemantics(
1229 1230
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1231
            TestSemantics(
1232 1233
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1234
            TestSemantics(
1235 1236
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
1237
            TestSemantics(
1238 1239 1240 1241 1242 1243
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
          ],
        ),
        ignoreTransform: true,
        ignoreRect: true,
1244 1245
        ignoreId: true,
      ),
1246
    );
1247 1248

    handle.dispose();
1249 1250
    semantics.dispose();
  });
1251 1252

  testWidgets('Semantics excludeSemantics ignores children', (WidgetTester tester) async {
1253 1254
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(Semantics(
1255 1256 1257
      label: 'label',
      excludeSemantics: true,
      textDirection: TextDirection.ltr,
1258
      child: Semantics(
1259 1260 1261 1262 1263 1264
        label: 'other label',
        textDirection: TextDirection.ltr,
      ),
    ));

    expect(semantics, hasSemantics(
1265
      TestSemantics(
1266
        children: <TestSemantics>[
1267
          TestSemantics(
1268 1269 1270 1271
            label: 'label',
            textDirection: TextDirection.ltr,
          ),
        ],
1272 1273 1274 1275 1276
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
    ));
1277 1278
    semantics.dispose();
  });
1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665

  testWidgets('Can change handlers', (WidgetTester tester) async {
    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onTap: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasTapAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDismiss: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDismissAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onLongPress: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasLongPressAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollLeft: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollLeftAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollRight: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollRightAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollUp: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollUpAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollDown: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollDownAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onIncrease: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasIncreaseAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDecrease: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDecreaseAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onCopy: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasCopyAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onCut: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasCutAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onPaste: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasPasteAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onSetSelection: (TextSelection _) {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasSetSelectionAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDidGainAccessibilityFocus: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDidGainAccessibilityFocusAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDidLoseAccessibilityFocus: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDidLoseAccessibilityFocusAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));
  });
1666 1667 1668 1669 1670 1671

  testWidgets('Semantics with zero transform gets dropped', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/110671.
    // Construct a widget tree that will end up with a fitted box that applies
    // a zero transform because it does not actually draw its children.
    // Assert that this subtree gets dropped (the root node has no children).
1672
    await tester.pumpWidget(const Column(
1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692
      children: <Widget>[
        SizedBox(
          height: 0,
          width: 500,
          child: FittedBox(
            child: SizedBox(
              height: 55,
              width: 266,
              child: SingleChildScrollView(child: Column()),
            ),
          ),
        ),
      ],
    ));

    final SemanticsNode node = RendererBinding.instance.renderView.debugSemantics!;

    expect(node.transform, null); // Make sure the zero transform didn't end up on the root somehow.
    expect(node.childrenCount, 0);
  });
1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804

  testWidgets('blocking user interaction works on explicit child node.', (WidgetTester tester) async {
    final UniqueKey key1 = UniqueKey();
    final UniqueKey key2 = UniqueKey();
    await tester.pumpWidget(
      MaterialApp(
        home: Semantics(
          blockUserActions: true,
          explicitChildNodes: true,
          child: Column(
            children: <Widget>[
              Semantics(
                key: key1,
                label: 'label1',
                onTap: () {},
                child: const SizedBox(width: 10, height: 10),
              ),
              Semantics(
                key: key2,
                label: 'label2',
                onTap: () {},
                child: const SizedBox(width: 10, height: 10),
              ),
            ],
          ),
        ),
      ),
    );
    expect(
      tester.getSemantics(find.byKey(key1)),
      // Tap action is blocked.
      matchesSemantics(
        label: 'label1',
      ),
    );
    expect(
      tester.getSemantics(find.byKey(key2)),
      // Tap action is blocked.
      matchesSemantics(
        label: 'label2',
      ),
    );
  });

  testWidgets('blocking user interaction on a merged child', (WidgetTester tester) async {
    final UniqueKey key = UniqueKey();
    await tester.pumpWidget(
      MaterialApp(
        home: Semantics(
          key: key,
          container: true,
          child: Column(
            children: <Widget>[
              Semantics(
                blockUserActions: true,
                label: 'label1',
                onTap: () { },
                child: const SizedBox(width: 10, height: 10),
              ),
              Semantics(
                label: 'label2',
                onLongPress: () { },
                child: const SizedBox(width: 10, height: 10),
              ),
            ],
          ),
        ),
      ),
    );
    expect(
      tester.getSemantics(find.byKey(key)),
      // Tap action in label1 is blocked,
      matchesSemantics(
        label: 'label1\nlabel2',
        hasLongPressAction: true,
      ),
    );
  });

  testWidgets('does not merge conflicting actions even if one of them is blocked', (WidgetTester tester) async {
    final UniqueKey key = UniqueKey();
    await tester.pumpWidget(
      MaterialApp(
        home: Semantics(
          key: key,
          container: true,
          child: Column(
            children: <Widget>[
              Semantics(
                blockUserActions: true,
                label: 'label1',
                onTap: () { },
                child: const SizedBox(width: 10, height: 10),
              ),
              Semantics(
                label: 'label2',
                onTap: () { },
                child: const SizedBox(width: 10, height: 10),
              ),
            ],
          ),
        ),
      ),
    );
    final SemanticsNode node = tester.getSemantics(find.byKey(key));
    expect(
      node,
      matchesSemantics(
        children: <Matcher>[containsSemantics(label: 'label1'), containsSemantics(label: 'label2')],
      ),
    );
  });
1805 1806 1807
}

class CustomSortKey extends OrdinalSortKey {
1808
  const CustomSortKey(super.order, {super.name});
1809
}