526 lines
16 KiB
Dart
526 lines
16 KiB
Dart
// 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 'package:flutter/material.dart';
|
|
import 'package:flutter/semantics.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
testWidgets('Radio group control test', (WidgetTester tester) async {
|
|
final key0 = UniqueKey();
|
|
final key1 = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
Material(
|
|
child: TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Radio<int>(key: key0, value: 0),
|
|
Radio<int>(key: key1, value: 1),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key0)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
|
);
|
|
|
|
await tester.tap(find.byKey(key0));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
tester.getSemantics(find.byKey(key0)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: true, isEnabled: true),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
|
);
|
|
|
|
await tester.tap(find.byKey(key1));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
tester.getSemantics(find.byKey(key0)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: true, isEnabled: true),
|
|
);
|
|
});
|
|
|
|
testWidgets('Radio group can have disabled radio', (WidgetTester tester) async {
|
|
final key0 = UniqueKey();
|
|
final key1 = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
Material(
|
|
child: TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Radio<int>(key: key0, value: 0, enabled: false),
|
|
Radio<int>(key: key1, value: 1),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key0)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: false),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
|
);
|
|
|
|
await tester.tap(find.byKey(key0));
|
|
await tester.pumpAndSettle();
|
|
// Can't be select because the radio is disabled.
|
|
expect(
|
|
tester.getSemantics(find.byKey(key0)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: false),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
isSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
|
|
);
|
|
});
|
|
|
|
testWidgets('Radio group will not merge up', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Material(
|
|
child: Semantics(
|
|
container: true,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Checkbox(value: true, onChanged: (bool? value) {}),
|
|
const TestRadioGroup<int>(
|
|
child: Column(children: <Widget>[Radio<int>(value: 0), Radio<int>(value: 1)]),
|
|
),
|
|
Checkbox(value: true, onChanged: (bool? value) {}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final SemanticsNode radioGroup = tester.getSemantics(find.byType(RadioGroup<int>));
|
|
expect(radioGroup.childrenCount, 2);
|
|
});
|
|
|
|
testWidgets('Radio group can use arrow key', (WidgetTester tester) async {
|
|
final key0 = UniqueKey();
|
|
final key1 = UniqueKey();
|
|
final key2 = UniqueKey();
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Radio<int>(key: key0, focusNode: focusNode, value: 0),
|
|
Radio<int>(key: key1, value: 1),
|
|
Radio<int>(key: key2, value: 2),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
|
|
find.byType(TestRadioGroup<int>),
|
|
);
|
|
|
|
await tester.tap(find.byKey(key0));
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(focusNode.hasFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 1);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 2);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
// Wrap around
|
|
expect(state.groupValue, 0);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
// Wrap around
|
|
expect(state.groupValue, 2);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
// Wrap around
|
|
expect(state.groupValue, 1);
|
|
});
|
|
|
|
testWidgets('Radio group arrow key skips disabled radio', (WidgetTester tester) async {
|
|
final key0 = UniqueKey();
|
|
final key1 = UniqueKey();
|
|
final key2 = UniqueKey();
|
|
final focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Radio<int>(key: key0, focusNode: focusNode, value: 0),
|
|
Radio<int>(key: key1, enabled: false, value: 1),
|
|
Radio<int>(key: key2, value: 2),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
|
|
find.byType(TestRadioGroup<int>),
|
|
);
|
|
|
|
await tester.tap(find.byKey(key0));
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(focusNode.hasFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 2);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
// Wrap around
|
|
expect(state.groupValue, 0);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 2);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
// Wrap around
|
|
expect(state.groupValue, 2);
|
|
});
|
|
|
|
testWidgets('Radio group can tab in and out', (WidgetTester tester) async {
|
|
final key0 = UniqueKey();
|
|
final key1 = UniqueKey();
|
|
final key2 = UniqueKey();
|
|
final radio0 = FocusNode();
|
|
addTearDown(radio0.dispose);
|
|
final radio1 = FocusNode();
|
|
addTearDown(radio1.dispose);
|
|
final textFieldBefore = FocusNode();
|
|
addTearDown(textFieldBefore.dispose);
|
|
final textFieldAfter = FocusNode();
|
|
addTearDown(textFieldAfter.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Column(
|
|
children: <Widget>[
|
|
TextField(focusNode: textFieldBefore),
|
|
TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Radio<int>(key: key0, focusNode: radio0, value: 0),
|
|
Radio<int>(key: key1, focusNode: radio1, value: 1),
|
|
Radio<int>(key: key2, value: 2),
|
|
],
|
|
),
|
|
),
|
|
TextField(focusNode: textFieldAfter),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
textFieldBefore.requestFocus();
|
|
await tester.pump();
|
|
expect(textFieldBefore.hasFocus, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
await tester.pump();
|
|
// If no selected radio, focus the first.
|
|
expect(textFieldBefore.hasFocus, isFalse);
|
|
expect(radio0.hasFocus, isTrue);
|
|
|
|
// tab out the radio group.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
await tester.pump();
|
|
expect(radio0.hasFocus, isFalse);
|
|
expect(radio1.hasFocus, isFalse);
|
|
expect(textFieldAfter.hasFocus, isTrue);
|
|
|
|
// Select the radio 1
|
|
await tester.tap(find.byKey(key1));
|
|
await tester.pump();
|
|
final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
|
|
find.byType(TestRadioGroup<int>),
|
|
);
|
|
expect(state.groupValue, 1);
|
|
// focus textFieldAfter again.
|
|
textFieldAfter.requestFocus();
|
|
await tester.pump();
|
|
expect(textFieldAfter.hasFocus, isTrue);
|
|
|
|
// shift+tab in the radio again.
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
await tester.pump();
|
|
// Should focus selected radio
|
|
expect(radio0.hasFocus, isFalse);
|
|
expect(radio1.hasFocus, isTrue);
|
|
expect(textFieldAfter.hasFocus, isFalse);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/175258.
|
|
testWidgets('Radio group throws on multiple selection', (WidgetTester tester) async {
|
|
final key1 = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
const Radio<int>(value: 0),
|
|
Radio<int>(key: key1, value: 1),
|
|
const Radio<int>(value: 1),
|
|
const Radio<int>(value: 2),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.tap(find.byKey(key1));
|
|
await tester.pump();
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isA<FlutterError>().having(
|
|
(FlutterError e) => e.message,
|
|
'message',
|
|
"RadioGroupPolicy can't be used for a radio group that allows multiple selection.",
|
|
),
|
|
);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/175258.
|
|
testWidgets('Radio group does not throw when number of children decreases', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: RadioGroup<int>(
|
|
onChanged: (_) {},
|
|
groupValue: 4,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Radio<int>(value: 0),
|
|
Radio<int>(value: 1),
|
|
Radio<int>(value: 2),
|
|
Radio<int>(value: 3),
|
|
Radio<int>(value: 4),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: RadioGroup<int>(
|
|
onChanged: (_) {},
|
|
groupValue: 4,
|
|
child: const Column(
|
|
children: <Widget>[
|
|
Radio<int>(value: 1),
|
|
Radio<int>(value: 2),
|
|
Radio<int>(value: 3),
|
|
Radio<int>(value: 4),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/175511.
|
|
testWidgets('Radio group does not intercept key events when no radio is focused', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final log = <String>[];
|
|
late final shortcuts = <ShortcutActivator, Intent>{
|
|
const SingleActivator(LogicalKeyboardKey.arrowLeft): VoidCallbackIntent(() => log.add('←')),
|
|
const SingleActivator(LogicalKeyboardKey.arrowRight): VoidCallbackIntent(() => log.add('→')),
|
|
const SingleActivator(LogicalKeyboardKey.arrowDown): VoidCallbackIntent(() => log.add('↓')),
|
|
const SingleActivator(LogicalKeyboardKey.arrowUp): VoidCallbackIntent(() => log.add('↑')),
|
|
const SingleActivator(LogicalKeyboardKey.space): VoidCallbackIntent(() => log.add('_')),
|
|
};
|
|
|
|
final firstRadioFocusNode = FocusNode();
|
|
addTearDown(firstRadioFocusNode.dispose);
|
|
final textFieldFocusNode = FocusNode();
|
|
addTearDown(textFieldFocusNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Shortcuts(
|
|
shortcuts: shortcuts,
|
|
child: TestRadioGroup<int>(
|
|
child: Column(
|
|
children: <Widget>[
|
|
Radio<int>(focusNode: firstRadioFocusNode, value: 0),
|
|
const RadioListTile<int>(value: 1),
|
|
const Radio<int>(value: 2),
|
|
TextField(focusNode: textFieldFocusNode),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
|
|
find.byType(TestRadioGroup<int>),
|
|
);
|
|
|
|
// Focus on the first radio and toggle it.
|
|
firstRadioFocusNode.requestFocus();
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(firstRadioFocusNode.hasFocus, isTrue);
|
|
|
|
// Toggle the second radio with shortcut.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 1);
|
|
// Log is empty because radio group handles shortcuts.
|
|
expect(log, isEmpty);
|
|
|
|
// Toggle the first radio with shortcut.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, isEmpty);
|
|
|
|
// Move focus to the text field.
|
|
// Now radio group will ignore shortcuts as there are no focused radios.
|
|
textFieldFocusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify that shortcuts are not intercepted by the radio group.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, <String>['←']);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, <String>['←', '→']);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, <String>['←', '→', '↓']);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, <String>['←', '→', '↓', '↑']);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, <String>['←', '→', '↓', '↑', '_']);
|
|
|
|
log.clear();
|
|
expect(log, isEmpty);
|
|
|
|
// Focus on the first radio.
|
|
firstRadioFocusNode.requestFocus();
|
|
await tester.pump();
|
|
expect(state.groupValue, 0);
|
|
expect(firstRadioFocusNode.hasFocus, isTrue);
|
|
|
|
// Verify that radio group handles shortcuts again.
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 1);
|
|
expect(log, isEmpty);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
expect(state.groupValue, 0);
|
|
expect(log, isEmpty);
|
|
});
|
|
}
|
|
|
|
class TestRadioGroup<T> extends StatefulWidget {
|
|
const TestRadioGroup({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => TestRadioGroupState<T>();
|
|
}
|
|
|
|
class TestRadioGroupState<T> extends State<TestRadioGroup<T>> {
|
|
T? groupValue;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RadioGroup<T>(
|
|
onChanged: (T? newValue) {
|
|
setState(() {
|
|
groupValue = newValue;
|
|
});
|
|
},
|
|
groupValue: groupValue,
|
|
child: widget.child,
|
|
);
|
|
}
|
|
}
|