blob: 981a814e08934ee8936795d5e1c3d6a51661fcef [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:android_semantics_testing/android_semantics_testing.dart';
import 'package:android_semantics_testing/main.dart' as app;
import 'package:android_semantics_testing/test_constants.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
// 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,
];
const MethodChannel kSemanticsChannel = MethodChannel('semantics');
Future<void> setClipboard(String message) async {
final Completer<void> completer = Completer<void>();
Future<void> completeSetClipboard([Object? _]) async {
await kSemanticsChannel.invokeMethod<dynamic>('setClipboard', <String, dynamic>{
'message': message,
});
completer.complete();
}
if (SchedulerBinding.instance.hasScheduledFrame) {
SchedulerBinding.instance.addPostFrameCallback(completeSetClipboard);
} else {
completeSetClipboard();
}
await completer.future;
}
Future<AndroidSemanticsNode> getSemantics(Finder finder, WidgetTester tester) async {
final int id = tester.getSemantics(finder).id;
final Completer<String> completer = Completer<String>();
Future<void> completeSemantics([Object? _]) async {
final dynamic result = await kSemanticsChannel.invokeMethod<dynamic>('getSemanticsNode', <String, dynamic>{
'id': id,
});
completer.complete(json.encode(result));
}
if (SchedulerBinding.instance.hasScheduledFrame) {
SchedulerBinding.instance.addPostFrameCallback(completeSemantics);
} else {
completeSemantics();
}
return AndroidSemanticsNode.deserialize(await completer.future);
}
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('AccessibilityBridge', () {
group('TextField', () {
Future<void> prepareTextField(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.text(textFieldRoute));
await tester.pumpAndSettle();
// 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.
await setClipboard('Hello World');
await tester.pumpAndSettle();
}
testWidgets('TextField has correct Android semantics', (WidgetTester tester) async {
final Finder normalTextField = find.descendant(
of: find.byKey(const ValueKey<String>(normalTextFieldKeyValue)),
matching: find.byType(EditableText),
);
await prepareTextField(tester);
expect(
await getSemantics(normalTextField, tester),
hasAndroidSemantics(
className: AndroidClassName.editText,
isEditable: true,
isFocusable: true,
isFocused: false,
isPassword: false,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
// We can't predict the a11y focus when the screen changes.
ignoredActions: ignoredAccessibilityFocusActions,
),
);
await tester.tap(normalTextField);
await tester.pumpAndSettle();
expect(
await getSemantics(normalTextField, tester),
hasAndroidSemantics(
className: AndroidClassName.editText,
isFocusable: true,
isFocused: true,
isEditable: true,
isPassword: false,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
AndroidSemanticsAction.paste,
AndroidSemanticsAction.setSelection,
AndroidSemanticsAction.setText,
],
// We can't predict the a11y focus when the screen changes.
ignoredActions: ignoredAccessibilityFocusActions,
),
);
await tester.enterText(normalTextField, 'hello world');
await tester.pumpAndSettle();
expect(
await getSemantics(normalTextField, tester),
hasAndroidSemantics(
text: 'hello world',
className: AndroidClassName.editText,
isFocusable: true,
isFocused: true,
isEditable: true,
isPassword: false,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
AndroidSemanticsAction.paste,
AndroidSemanticsAction.setSelection,
AndroidSemanticsAction.setText,
AndroidSemanticsAction.previousAtMovementGranularity,
],
// We can't predict the a11y focus when the screen changes.
ignoredActions: ignoredAccessibilityFocusActions,
),
);
}, timeout: Timeout.none);
testWidgets('password TextField has correct Android semantics', (WidgetTester tester) async {
final Finder passwordTextField = find.descendant(
of: find.byKey(const ValueKey<String>(passwordTextFieldKeyValue)),
matching: find.byType(EditableText),
);
await prepareTextField(tester);
expect(
await getSemantics(passwordTextField, tester),
hasAndroidSemantics(
className: AndroidClassName.editText,
isEditable: true,
isFocusable: true,
isFocused: false,
isPassword: true,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
// We can't predict the a11y focus when the screen changes.
ignoredActions: ignoredAccessibilityFocusActions,
),
);
await tester.tap(passwordTextField);
await tester.pumpAndSettle();
expect(
await getSemantics(passwordTextField, tester),
hasAndroidSemantics(
className: AndroidClassName.editText,
isFocusable: true,
isFocused: true,
isEditable: true,
isPassword: true,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
AndroidSemanticsAction.paste,
AndroidSemanticsAction.setSelection,
AndroidSemanticsAction.setText,
],
// We can't predict the a11y focus when the screen changes.
ignoredActions: ignoredAccessibilityFocusActions,
),
);
await tester.enterText(passwordTextField, 'hello world');
await tester.pumpAndSettle();
expect(
await getSemantics(passwordTextField, tester),
hasAndroidSemantics(
text: '\u{2022}' * ('hello world'.length),
className: AndroidClassName.editText,
isFocusable: true,
isFocused: true,
isEditable: true,
isPassword: true,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
AndroidSemanticsAction.paste,
AndroidSemanticsAction.setSelection,
AndroidSemanticsAction.setText,
AndroidSemanticsAction.previousAtMovementGranularity,
],
// We can't predict the a11y focus when the screen changes.
ignoredActions: ignoredAccessibilityFocusActions,
),
);
}, timeout: Timeout.none);
});
group('SelectionControls', () {
Future<void> prepareSelectionControls(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.text(selectionControlsRoute));
await tester.pumpAndSettle();
}
testWidgets('Checkbox has correct Android semantics', (WidgetTester tester) async {
final Finder checkbox = find.byKey(const ValueKey<String>(checkboxKeyValue));
final Finder disabledCheckbox = find.byKey(const ValueKey<String>(disabledCheckboxKeyValue));
await prepareSelectionControls(tester);
expect(
await getSemantics(checkbox, tester),
hasAndroidSemantics(
className: AndroidClassName.checkBox,
isChecked: false,
isCheckable: true,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
await tester.tap(checkbox);
await tester.pumpAndSettle();
expect(
await getSemantics(checkbox, tester),
hasAndroidSemantics(
className: AndroidClassName.checkBox,
isChecked: true,
isCheckable: true,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
expect(
await getSemantics(disabledCheckbox, tester),
hasAndroidSemantics(
className: AndroidClassName.checkBox,
isCheckable: true,
isEnabled: false,
ignoredActions: ignoredAccessibilityFocusActions,
actions: const <AndroidSemanticsAction>[],
),
);
}, timeout: Timeout.none);
testWidgets('Radio has correct Android semantics', (WidgetTester tester) async {
final Finder radio = find.byKey(const ValueKey<String>(radio2KeyValue));
await prepareSelectionControls(tester);
expect(
await getSemantics(radio, tester),
hasAndroidSemantics(
className: AndroidClassName.radio,
isChecked: false,
isCheckable: true,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
await tester.tap(radio);
await tester.pumpAndSettle();
expect(
await getSemantics(radio, tester),
hasAndroidSemantics(
className: AndroidClassName.radio,
isChecked: true,
isCheckable: true,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
}, timeout: Timeout.none);
testWidgets('Switch has correct Android semantics', (WidgetTester tester) async {
final Finder switchFinder = find.byKey(const ValueKey<String>(switchKeyValue));
await prepareSelectionControls(tester);
expect(
await getSemantics(switchFinder, tester),
hasAndroidSemantics(
className: AndroidClassName.toggleSwitch,
isChecked: false,
isCheckable: true,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
await tester.tap(switchFinder);
await tester.pumpAndSettle();
expect(
await getSemantics(switchFinder, tester),
hasAndroidSemantics(
className: AndroidClassName.toggleSwitch,
isChecked: true,
isCheckable: true,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
}, timeout: Timeout.none);
// Regression test for https://github.com/flutter/flutter/issues/20820.
testWidgets('Switch can be labeled', (WidgetTester tester) async {
final Finder switchFinder = find.byKey(const ValueKey<String>(labeledSwitchKeyValue));
await prepareSelectionControls(tester);
expect(
await getSemantics(switchFinder, tester),
hasAndroidSemantics(
className: AndroidClassName.toggleSwitch,
isChecked: false,
isCheckable: true,
isEnabled: true,
isFocusable: true,
contentDescription: switchLabel,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
}, timeout: Timeout.none);
});
group('Popup Controls', () {
Future<void> preparePopupControls(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.text(popupControlsRoute));
await tester.pumpAndSettle();
}
testWidgets('Popup Menu has correct Android semantics', (WidgetTester tester) async {
final Finder popupButton = find.byKey(const ValueKey<String>(popupButtonKeyValue));
await preparePopupControls(tester);
expect(
await getSemantics(popupButton, tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
await tester.tap(popupButton);
await tester.pumpAndSettle();
try {
for (final String item in popupItems) {
expect(
await getSemantics(find.byKey(ValueKey<String>('$popupKeyValue.$item')), tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
reason: "Popup $item doesn't have the right semantics",
);
}
await tester.tap(find.byKey(ValueKey<String>('$popupKeyValue.${popupItems.first}')));
await tester.pumpAndSettle();
// Pop up the menu again, to verify that TalkBack gets the right answer
// more than just the first time.
await tester.tap(popupButton);
await tester.pumpAndSettle();
for (final String item in popupItems) {
expect(
await getSemantics(find.byKey(ValueKey<String>('$popupKeyValue.$item')), tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
reason: "Popup $item doesn't have the right semantics the second time",
);
}
} finally {
await tester.tap(find.byKey(ValueKey<String>('$popupKeyValue.${popupItems.first}')));
}
}, timeout: Timeout.none);
testWidgets('Dropdown Menu has correct Android semantics', (WidgetTester tester) async {
final Finder dropdownButton = find.byKey(const ValueKey<String>(dropdownButtonKeyValue));
await preparePopupControls(tester);
expect(
await getSemantics(dropdownButton, tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
await tester.tap(dropdownButton);
await tester.pumpAndSettle();
try {
for (final String item in popupItems) {
// 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.byKey(ValueKey<String>('$dropdownKeyValue.$item')),
),
tester,
),
hasAndroidSemantics(
className: AndroidClassName.view,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
reason: "Dropdown $item doesn't have the right semantics",
);
}
await tester.tap(
find.descendant(
of: find.byType(Scrollable),
matching: find.byKey(ValueKey<String>('$dropdownKeyValue.${popupItems.first}')),
),
);
await tester.pumpAndSettle();
// Pop up the dropdown again, to verify that TalkBack gets the right answer
// more than just the first time.
await tester.tap(dropdownButton);
await tester.pumpAndSettle();
for (final String item in popupItems) {
// 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.byKey(ValueKey<String>('$dropdownKeyValue.$item')),
),
tester,
),
hasAndroidSemantics(
className: AndroidClassName.view,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
reason: "Dropdown $item doesn't have the right semantics the second time.",
);
}
} finally {
await tester.tap(
find.descendant(
of: find.byType(Scrollable),
matching: find.byKey(ValueKey<String>('$dropdownKeyValue.${popupItems.first}')),
),
);
}
}, timeout: Timeout.none);
testWidgets('Modal alert dialog has correct Android semantics', (WidgetTester tester) async {
final Finder alertButton = find.byKey(const ValueKey<String>(alertButtonKeyValue));
await preparePopupControls(tester);
expect(
await getSemantics(alertButton, tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
);
await tester.tap(alertButton);
await tester.pumpAndSettle();
try {
expect(
await getSemantics(find.byKey(const ValueKey<String>('$alertKeyValue.OK')), tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
reason: "Alert OK button doesn't have the right semantics",
);
for (final String item in <String>['Title', 'Body1', 'Body2']) {
expect(
await getSemantics(find.byKey(ValueKey<String>('$alertKeyValue.$item')), tester),
hasAndroidSemantics(
className: AndroidClassName.view,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[],
),
reason: "Alert $item button doesn't have the right semantics",
);
}
await tester.tap(find.byKey(const ValueKey<String>('$alertKeyValue.OK')));
await tester.pumpAndSettle();
// Pop up the alert again, to verify that TalkBack gets the right answer
// more than just the first time.
await tester.tap(alertButton);
await tester.pumpAndSettle();
expect(
await getSemantics(find.byKey(const ValueKey<String>('$alertKeyValue.OK')), tester),
hasAndroidSemantics(
className: AndroidClassName.button,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[
AndroidSemanticsAction.click,
],
),
reason: "Alert OK button doesn't have the right semantics",
);
for (final String item in <String>['Title', 'Body1', 'Body2']) {
expect(
await getSemantics(find.byKey(ValueKey<String>('$alertKeyValue.$item')), tester),
hasAndroidSemantics(
className: AndroidClassName.view,
isChecked: false,
isCheckable: false,
isEnabled: true,
isFocusable: true,
ignoredActions: ignoredAccessibilityFocusActions,
actions: <AndroidSemanticsAction>[],
),
reason: "Alert $item button doesn't have the right semantics",
);
}
} finally {
await tester.tap(find.byKey(const ValueKey<String>('$alertKeyValue.OK')));
}
}, timeout: Timeout.none);
});
group('Headings', () {
Future<void> prepareHeading(WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.text(headingsRoute));
await tester.pumpAndSettle();
}
testWidgets('AppBar title has correct Android heading semantics', (WidgetTester tester) async {
await prepareHeading(tester);
expect(
await getSemantics(find.byKey(const ValueKey<String>(appBarTitleKeyValue)), tester),
hasAndroidSemantics(isHeading: true),
);
}, timeout: Timeout.none);
testWidgets('body text does not have Android heading semantics', (WidgetTester tester) async {
await prepareHeading(tester);
expect(
await getSemantics(find.byKey(const ValueKey<String>(bodyTextKeyValue)), tester),
hasAndroidSemantics(isHeading: false),
);
}, timeout: Timeout.none);
});
});
}