main_test.dart 25.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io' as io;

import 'package:android_semantics_testing/android_semantics_testing.dart';
8
import 'package:android_semantics_testing/test_constants.dart';
9
import 'package:flutter_driver/flutter_driver.dart';
10
import 'package:path/path.dart' as path;
11
import 'package:pub_semver/pub_semver.dart';
12
import 'package:test/test.dart' hide isInstanceOf;
13

14 15 16 17 18 19 20 21
// The accessibility focus actions are added when a semantics node receives or
// lose accessibility focus. This test ignores these actions since it is hard to
// predict which node has the accessibility focus after a screen changes.
const List<AndroidSemanticsAction> ignoredAccessibilityFocusActions = <AndroidSemanticsAction>[
  AndroidSemanticsAction.accessibilityFocus,
  AndroidSemanticsAction.clearAccessibilityFocus,
];

22
String adbPath() {
23
  final String androidHome = io.Platform.environment['ANDROID_HOME'] ?? io.Platform.environment['ANDROID_SDK_ROOT']!;
24 25 26 27 28 29
  if (androidHome == null) {
    return 'adb';
  } else {
    return path.join(androidHome, 'platform-tools', 'adb');
  }
}
30 31 32

void main() {
  group('AccessibilityBridge', () {
33
    late FlutterDriver driver;
34 35 36
    Future<AndroidSemanticsNode> getSemantics(SerializableFinder finder) async {
      final int id = await driver.getSemanticsId(finder);
      final String data = await driver.requestData('getSemanticsNode#$id');
37
      return AndroidSemanticsNode.deserialize(data);
38 39
    }

40
    // The version of TalkBack running on the device.
41
    Version? talkbackVersion;
42 43 44 45 46 47 48 49 50 51 52 53

    Future<Version> getTalkbackVersion() async {
      final io.ProcessResult result = await io.Process.run(adbPath(), const <String>[
        'shell',
        'dumpsys',
        'package',
        'com.google.android.marvin.talkback',
      ]);
      if (result.exitCode != 0) {
        throw Exception('Failed to get TalkBack version: ${result.stdout as String}\n${result.stderr as String}');
      }
      final List<String> lines = (result.stdout as String).split('\n');
54
      String? version;
55 56 57 58 59 60 61 62 63 64 65 66
      for (final String line in lines) {
        if (line.contains('versionName')) {
          version = line.replaceAll(RegExp(r'\s*versionName='), '');
          break;
        }
      }
      if (version == null) {
        throw Exception('Unable to determine TalkBack version.');
      }

      // Android doesn't quite use semver, so convert the version string to semver form.
      final RegExp startVersion = RegExp(r'(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(\.(?<build>\d+))?');
67
      final RegExpMatch? match = startVersion.firstMatch(version);
68 69 70 71
      if (match == null) {
        return Version(0, 0, 0);
      }
      return Version(
72 73 74
        int.parse(match.namedGroup('major')!),
        int.parse(match.namedGroup('minor')!),
        int.parse(match.namedGroup('patch')!),
75 76 77 78
        build: match.namedGroup('build'),
      );
    }

79 80
    setUpAll(() async {
      driver = await FlutterDriver.connect();
81 82 83
      talkbackVersion ??= await getTalkbackVersion();
      print('TalkBack version is $talkbackVersion');

84
      // Say the magic words..
85
      final io.Process run = await io.Process.start(adbPath(), const <String>[
86 87 88 89 90 91 92 93 94 95 96 97
        'shell',
        'settings',
        'put',
        'secure',
        'enabled_accessibility_services',
        'com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService',
      ]);
      await run.exitCode;
    });

    tearDownAll(() async {
      // ... And turn it off again
98
      final io.Process run = await io.Process.start(adbPath(), const <String>[
99 100 101 102 103 104 105 106
        'shell',
        'settings',
        'put',
        'secure',
        'enabled_accessibility_services',
        'null',
      ]);
      await run.exitCode;
107
      driver.close();
108
    });
109 110 111 112

    group('TextField', () {
      setUpAll(() async {
        await driver.tap(find.text(textFieldRoute));
113 114
        // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
        await Future<void>.delayed(const Duration(milliseconds: 500));
115 116 117 118 119 120 121 122

        // The text selection menu and related semantics vary depending on if
        // the clipboard contents are pasteable. Copy some text into the
        // clipboard to make sure these tests always run with pasteable content
        // in the clipboard.
        // Ideally this should test the case where there is nothing on the
        // clipboard as well, but there is no reliable way to clear the
        // clipboard on Android devices.
123
        await driver.requestData('setClipboard#Hello World');
124
        await Future<void>.delayed(const Duration(milliseconds: 500));
125 126 127
      });

      test('TextField has correct Android semantics', () async {
128 129 130 131 132
        final SerializableFinder normalTextField = find.descendant(
          of: find.byValueKey(normalTextFieldKeyValue),
          matching: find.byType('Semantics'),
          firstMatchOnly: true,
        );
133 134 135 136 137 138 139 140 141 142 143
        expect(
          await getSemantics(normalTextField),
          hasAndroidSemantics(
            className: AndroidClassName.editText,
            isEditable: true,
            isFocusable: true,
            isFocused: false,
            isPassword: false,
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
144
            // We can't predict the a11y focus when the screen changes.
145
            ignoredActions: ignoredAccessibilityFocusActions,
146 147
          ),
        );
148 149

        await driver.tap(normalTextField);
150
        // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
151
        await Future<void>.delayed(const Duration(milliseconds: 500));
152

153 154 155 156 157 158 159 160 161 162
        expect(
          await getSemantics(normalTextField),
          hasAndroidSemantics(
            className: AndroidClassName.editText,
            isFocusable: true,
            isFocused: true,
            isEditable: true,
            isPassword: false,
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
163
              AndroidSemanticsAction.copy,
164
              AndroidSemanticsAction.setSelection,
165
              AndroidSemanticsAction.setText,
166
            ],
167 168
            // We can't predict the a11y focus when the screen changes.
            ignoredActions: ignoredAccessibilityFocusActions,
169 170
          ),
        );
171 172

        await driver.enterText('hello world');
173
        // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
174
        await Future<void>.delayed(const Duration(milliseconds: 500));
175

176 177 178 179 180 181 182 183 184 185 186 187 188
        expect(
          await getSemantics(normalTextField),
          hasAndroidSemantics(
            text: 'hello world',
            className: AndroidClassName.editText,
            isFocusable: true,
            isFocused: true,
            isEditable: true,
            isPassword: false,
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
              AndroidSemanticsAction.copy,
              AndroidSemanticsAction.setSelection,
189
              AndroidSemanticsAction.setText,
190
              AndroidSemanticsAction.previousAtMovementGranularity,
191
            ],
192 193
            // We can't predict the a11y focus when the screen changes.
            ignoredActions: ignoredAccessibilityFocusActions,
194 195
          ),
        );
196
      }, timeout: Timeout.none);
197 198

      test('password TextField has correct Android semantics', () async {
199 200 201 202 203
        final SerializableFinder passwordTextField = find.descendant(
          of: find.byValueKey(passwordTextFieldKeyValue),
          matching: find.byType('Semantics'),
          firstMatchOnly: true,
        );
204 205 206 207 208 209 210 211 212 213 214
        expect(
          await getSemantics(passwordTextField),
          hasAndroidSemantics(
            className: AndroidClassName.editText,
            isEditable: true,
            isFocusable: true,
            isFocused: false,
            isPassword: true,
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
215 216
            // We can't predict the a11y focus when the screen changes.
            ignoredActions: ignoredAccessibilityFocusActions,
217 218
          ),
        );
219 220

        await driver.tap(passwordTextField);
221 222
        // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
        await Future<void>.delayed(const Duration(milliseconds: 500));
223

224 225 226 227 228 229 230 231 232 233
        expect(
          await getSemantics(passwordTextField),
          hasAndroidSemantics(
            className: AndroidClassName.editText,
            isFocusable: true,
            isFocused: true,
            isEditable: true,
            isPassword: true,
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
234
              AndroidSemanticsAction.copy,
235
              AndroidSemanticsAction.setSelection,
236
              AndroidSemanticsAction.setText,
237
            ],
238 239
            // We can't predict the a11y focus when the screen changes.
            ignoredActions: ignoredAccessibilityFocusActions,
240 241
          ),
        );
242 243

        await driver.enterText('hello world');
244 245
        // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
        await Future<void>.delayed(const Duration(milliseconds: 500));
246

247 248 249 250 251 252 253 254 255 256 257
        expect(
          await getSemantics(passwordTextField),
          hasAndroidSemantics(
            text: '\u{2022}' * ('hello world'.length),
            className: AndroidClassName.editText,
            isFocusable: true,
            isFocused: true,
            isEditable: true,
            isPassword: true,
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
258
              AndroidSemanticsAction.copy,
259
              AndroidSemanticsAction.setSelection,
260
              AndroidSemanticsAction.setText,
261
              AndroidSemanticsAction.previousAtMovementGranularity,
262
            ],
263 264
            // We can't predict the a11y focus when the screen changes.
            ignoredActions: ignoredAccessibilityFocusActions,
265 266
          ),
        );
267
      }, timeout: Timeout.none);
268 269 270 271

      tearDownAll(() async {
        await driver.tap(find.byValueKey('back'));
      });
272
    });
273

274 275 276 277 278 279
    group('SelectionControls', () {
      setUpAll(() async {
        await driver.tap(find.text(selectionControlsRoute));
      });

      test('Checkbox has correct Android semantics', () async {
280
        Future<AndroidSemanticsNode> getCheckboxSemantics(String key) async {
281
          return getSemantics(find.byValueKey(key));
282
        }
283
        expect(
284
          await getCheckboxSemantics(checkboxKeyValue),
285 286 287 288 289 290
          hasAndroidSemantics(
            className: AndroidClassName.checkBox,
            isChecked: false,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
291
            ignoredActions: ignoredAccessibilityFocusActions,
292 293 294 295 296
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
297 298 299

        await driver.tap(find.byValueKey(checkboxKeyValue));

300
        expect(
301
          await getCheckboxSemantics(checkboxKeyValue),
302 303 304 305 306 307
          hasAndroidSemantics(
            className: AndroidClassName.checkBox,
            isChecked: true,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
308
            ignoredActions: ignoredAccessibilityFocusActions,
309 310 311 312 313 314
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
        expect(
315
          await getCheckboxSemantics(disabledCheckboxKeyValue),
316 317 318 319
          hasAndroidSemantics(
            className: AndroidClassName.checkBox,
            isCheckable: true,
            isEnabled: false,
320 321
            ignoredActions: ignoredAccessibilityFocusActions,
            actions: const <AndroidSemanticsAction>[],
322 323
          ),
        );
324
      }, timeout: Timeout.none);
325
      test('Radio has correct Android semantics', () async {
326
        Future<AndroidSemanticsNode> getRadioSemantics(String key) async {
327
          return getSemantics(find.byValueKey(key));
328
        }
329
        expect(
330
          await getRadioSemantics(radio2KeyValue),
331 332 333 334 335 336
          hasAndroidSemantics(
            className: AndroidClassName.radio,
            isChecked: false,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
337
            ignoredActions: ignoredAccessibilityFocusActions,
338 339 340 341 342
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
343 344 345

        await driver.tap(find.byValueKey(radio2KeyValue));

346
        expect(
347
          await getRadioSemantics(radio2KeyValue),
348 349 350 351 352 353
          hasAndroidSemantics(
            className: AndroidClassName.radio,
            isChecked: true,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
354
            ignoredActions: ignoredAccessibilityFocusActions,
355 356 357 358 359
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
360
      }, timeout: Timeout.none);
361
      test('Switch has correct Android semantics', () async {
362
        Future<AndroidSemanticsNode> getSwitchSemantics(String key) async {
363
          return getSemantics(find.byValueKey(key));
364
        }
365
        expect(
366
          await getSwitchSemantics(switchKeyValue),
367 368 369 370 371 372
          hasAndroidSemantics(
            className: AndroidClassName.toggleSwitch,
            isChecked: false,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
373
            ignoredActions: ignoredAccessibilityFocusActions,
374 375 376 377 378
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
379 380 381

        await driver.tap(find.byValueKey(switchKeyValue));

382
        expect(
383
          await getSwitchSemantics(switchKeyValue),
384 385 386 387 388 389
          hasAndroidSemantics(
            className: AndroidClassName.toggleSwitch,
            isChecked: true,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
390
            ignoredActions: ignoredAccessibilityFocusActions,
391 392 393 394 395
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
396
      }, timeout: Timeout.none);
397 398 399

      // Regression test for https://github.com/flutter/flutter/issues/20820.
      test('Switch can be labeled', () async {
400
        Future<AndroidSemanticsNode> getSwitchSemantics(String key) async {
401
          return getSemantics(find.byValueKey(key));
402
        }
403
        expect(
404
          await getSwitchSemantics(labeledSwitchKeyValue),
405 406 407 408 409 410 411
          hasAndroidSemantics(
            className: AndroidClassName.toggleSwitch,
            isChecked: false,
            isCheckable: true,
            isEnabled: true,
            isFocusable: true,
            contentDescription: switchLabel,
412
            ignoredActions: ignoredAccessibilityFocusActions,
413 414 415 416 417
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );
418
      }, timeout: Timeout.none);
419 420 421 422 423 424 425 426 427 428 429 430 431

      tearDownAll(() async {
        await driver.tap(find.byValueKey('back'));
      });
    });

    group('Popup Controls', () {
      setUpAll(() async {
        await driver.tap(find.text(popupControlsRoute));
      });

      test('Popup Menu has correct Android semantics', () async {
        expect(
432
          await getSemantics(find.byValueKey(popupButtonKeyValue)),
433 434 435 436 437 438
          hasAndroidSemantics(
            className: AndroidClassName.button,
            isChecked: false,
            isCheckable: false,
            isEnabled: true,
            isFocusable: true,
439
            ignoredActions: ignoredAccessibilityFocusActions,
440 441 442 443 444 445 446 447 448 449 450 451
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );

        await driver.tap(find.byValueKey(popupButtonKeyValue));
        try {
          // We have to wait wall time here because we're waiting for TalkBack to
          // catch up.
          await Future<void>.delayed(const Duration(milliseconds: 1500));

452
          for (final String item in popupItems) {
453
            expect(
454 455 456 457 458 459 460
                await getSemantics(find.byValueKey('$popupKeyValue.$item')),
                hasAndroidSemantics(
                  className: AndroidClassName.button,
                  isChecked: false,
                  isCheckable: false,
                  isEnabled: true,
                  isFocusable: true,
461
                  ignoredActions: ignoredAccessibilityFocusActions,
462 463 464 465 466
                  actions: <AndroidSemanticsAction>[
                    AndroidSemanticsAction.click,
                  ],
                ),
                reason: "Popup $item doesn't have the right semantics");
467 468 469 470 471 472 473 474
          }
          await driver.tap(find.byValueKey('$popupKeyValue.${popupItems.first}'));

          // Pop up the menu again, to verify that TalkBack gets the right answer
          // more than just the first time.
          await driver.tap(find.byValueKey(popupButtonKeyValue));
          await Future<void>.delayed(const Duration(milliseconds: 1500));

475
          for (final String item in popupItems) {
476 477 478
            expect(
                await getSemantics(find.byValueKey('$popupKeyValue.$item')),
                hasAndroidSemantics(
479
                  className: AndroidClassName.button,
480 481 482 483
                  isChecked: false,
                  isCheckable: false,
                  isEnabled: true,
                  isFocusable: true,
484
                  ignoredActions: ignoredAccessibilityFocusActions,
485
                  actions: <AndroidSemanticsAction>[
486
                    AndroidSemanticsAction.click,
487 488 489 490 491 492 493
                  ],
                ),
                reason: "Popup $item doesn't have the right semantics the second time");
          }
        } finally {
          await driver.tap(find.byValueKey('$popupKeyValue.${popupItems.first}'));
        }
494
      }, timeout: Timeout.none);
495 496 497 498 499 500 501 502 503 504

      test('Dropdown Menu has correct Android semantics', () async {
        expect(
          await getSemantics(find.byValueKey(dropdownButtonKeyValue)),
          hasAndroidSemantics(
            className: AndroidClassName.button,
            isChecked: false,
            isCheckable: false,
            isEnabled: true,
            isFocusable: true,
505
            ignoredActions: ignoredAccessibilityFocusActions,
506 507 508 509 510 511 512 513 514 515
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );

        await driver.tap(find.byValueKey(dropdownButtonKeyValue));
        try {
          await Future<void>.delayed(const Duration(milliseconds: 1500));

516
          for (final String item in popupItems) {
517 518 519 520 521 522 523 524 525 526 527 528 529
            // There are two copies of each item, so we want to find the version
            // that is in the overlay, not the one in the dropdown.
            expect(
                await getSemantics(find.descendant(
                  of: find.byType('Scrollable'),
                  matching: find.byValueKey('$dropdownKeyValue.$item'),
                )),
                hasAndroidSemantics(
                  className: AndroidClassName.view,
                  isChecked: false,
                  isCheckable: false,
                  isEnabled: true,
                  isFocusable: true,
530
                  ignoredActions: ignoredAccessibilityFocusActions,
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
                  actions: <AndroidSemanticsAction>[
                    AndroidSemanticsAction.click,
                  ],
                ),
                reason: "Dropdown $item doesn't have the right semantics");
          }
          await driver.tap(
            find.descendant(
              of: find.byType('Scrollable'),
              matching: find.byValueKey('$dropdownKeyValue.${popupItems.first}'),
            ),
          );

          // Pop up the dropdown again, to verify that TalkBack gets the right answer
          // more than just the first time.
          await driver.tap(find.byValueKey(dropdownButtonKeyValue));
          await Future<void>.delayed(const Duration(milliseconds: 1500));

549
          for (final String item in popupItems) {
550 551 552 553 554 555 556 557 558 559 560 561 562
            // There are two copies of each item, so we want to find the version
            // that is in the overlay, not the one in the dropdown.
            expect(
                await getSemantics(find.descendant(
                  of: find.byType('Scrollable'),
                  matching: find.byValueKey('$dropdownKeyValue.$item'),
                )),
                hasAndroidSemantics(
                  className: AndroidClassName.view,
                  isChecked: false,
                  isCheckable: false,
                  isEnabled: true,
                  isFocusable: true,
563
                  ignoredActions: ignoredAccessibilityFocusActions,
564 565 566 567 568 569 570 571 572 573 574 575 576 577
                  actions: <AndroidSemanticsAction>[
                    AndroidSemanticsAction.click,
                  ],
                ),
                reason: "Dropdown $item doesn't have the right semantics the second time.");
          }
        } finally {
          await driver.tap(
            find.descendant(
              of: find.byType('Scrollable'),
              matching: find.byValueKey('$dropdownKeyValue.${popupItems.first}'),
            ),
          );
        }
578
      }, timeout: Timeout.none);
579 580 581 582 583 584 585 586 587 588

      test('Modal alert dialog has correct Android semantics', () async {
        expect(
          await getSemantics(find.byValueKey(alertButtonKeyValue)),
          hasAndroidSemantics(
            className: AndroidClassName.button,
            isChecked: false,
            isCheckable: false,
            isEnabled: true,
            isFocusable: true,
589
            ignoredActions: ignoredAccessibilityFocusActions,
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
            actions: <AndroidSemanticsAction>[
              AndroidSemanticsAction.click,
            ],
          ),
        );

        await driver.tap(find.byValueKey(alertButtonKeyValue));
        try {
          await Future<void>.delayed(const Duration(milliseconds: 1500));

          expect(
              await getSemantics(find.byValueKey('$alertKeyValue.OK')),
              hasAndroidSemantics(
                className: AndroidClassName.button,
                isChecked: false,
                isCheckable: false,
                isEnabled: true,
                isFocusable: true,
608
                ignoredActions: ignoredAccessibilityFocusActions,
609 610 611 612 613 614
                actions: <AndroidSemanticsAction>[
                  AndroidSemanticsAction.click,
                ],
              ),
              reason: "Alert OK button doesn't have the right semantics");

615
          for (final String item in <String>['Title', 'Body1', 'Body2']) {
616 617 618 619 620 621 622 623
            expect(
                await getSemantics(find.byValueKey('$alertKeyValue.$item')),
                hasAndroidSemantics(
                  className: AndroidClassName.view,
                  isChecked: false,
                  isCheckable: false,
                  isEnabled: true,
                  isFocusable: true,
624 625
                  ignoredActions: ignoredAccessibilityFocusActions,
                  actions: <AndroidSemanticsAction>[],
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
                ),
                reason: "Alert $item button doesn't have the right semantics");
          }

          await driver.tap(find.byValueKey('$alertKeyValue.OK'));

          // Pop up the alert again, to verify that TalkBack gets the right answer
          // more than just the first time.
          await driver.tap(find.byValueKey(alertButtonKeyValue));
          await Future<void>.delayed(const Duration(milliseconds: 1500));

          expect(
              await getSemantics(find.byValueKey('$alertKeyValue.OK')),
              hasAndroidSemantics(
                className: AndroidClassName.button,
                isChecked: false,
                isCheckable: false,
                isEnabled: true,
                isFocusable: true,
645
                ignoredActions: ignoredAccessibilityFocusActions,
646 647 648 649 650 651
                actions: <AndroidSemanticsAction>[
                  AndroidSemanticsAction.click,
                ],
              ),
              reason: "Alert OK button doesn't have the right semantics");

652
          for (final String item in <String>['Title', 'Body1', 'Body2']) {
653 654 655 656 657 658 659 660
            expect(
                await getSemantics(find.byValueKey('$alertKeyValue.$item')),
                hasAndroidSemantics(
                  className: AndroidClassName.view,
                  isChecked: false,
                  isCheckable: false,
                  isEnabled: true,
                  isFocusable: true,
661 662
                  ignoredActions: ignoredAccessibilityFocusActions,
                  actions: <AndroidSemanticsAction>[],
663 664 665 666 667 668
                ),
                reason: "Alert $item button doesn't have the right semantics");
          }
        } finally {
          await driver.tap(find.byValueKey('$alertKeyValue.OK'));
        }
669
      }, timeout: Timeout.none);
670 671

      tearDownAll(() async {
672
        await Future<void>.delayed(const Duration(milliseconds: 500));
673 674 675
        await driver.tap(find.byValueKey('back'));
      });
    });
676 677 678 679 680 681 682 683 684 685 686

    group('Headings', () {
      setUpAll(() async {
        await driver.tap(find.text(headingsRoute));
      });

      test('AppBar title has correct Android heading semantics', () async {
        expect(
          await getSemantics(find.byValueKey(appBarTitleKeyValue)),
          hasAndroidSemantics(isHeading: true),
        );
687
      }, timeout: Timeout.none);
688 689 690 691 692 693

      test('body text does not have Android heading semantics', () async {
        expect(
          await getSemantics(find.byValueKey(bodyTextKeyValue)),
          hasAndroidSemantics(isHeading: false),
        );
694
      }, timeout: Timeout.none);
695 696 697 698 699 700

      tearDownAll(() async {
        await driver.tap(find.byValueKey('back'));
      });
    });

701 702
  });
}