blob: cc13240745f7c7a3ecd25b84b54d1c535e35205e [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.
@TestOn('!chrome') // entire file needs triage.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl');
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_TimePickerDialog');
class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({
Key? key,
required this.onChanged,
this.locale,
this.entryMode = TimePickerEntryMode.dial,
}) : super(key: key);
final ValueChanged<TimeOfDay?> onChanged;
final Locale? locale;
final TimePickerEntryMode entryMode;
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: locale,
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('X'),
onPressed: () async {
onChanged(await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
initialEntryMode: entryMode,
));
},
);
}
),
),
),
);
}
}
Future<Offset?> startPicker(
WidgetTester tester,
ValueChanged<TimeOfDay?> onChanged, {
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
}) async {
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US'), entryMode: entryMode));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
}
Future<void> finishPicker(WidgetTester tester) async {
final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(ElevatedButton)))!;
await tester.tap(find.text(materialLocalizations.okButtonLabel));
await tester.pumpAndSettle(const Duration(seconds: 1));
}
void main() {
group('Time picker - Dial', () {
_tests();
});
group('Time picker - Input', () {
_testsInput();
});
}
void _tests() {
testWidgets('tap-select an hour', (WidgetTester tester) async {
TimeOfDay? result;
Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
await tester.tapAt(Offset(center.dx, center.dy - 50.0)); // 12:00 AM
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));
center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
await tester.tapAt(Offset(center.dx + 50.0, center.dy));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 3, minute: 0)));
center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
await tester.tapAt(Offset(center.dx, center.dy + 50.0));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 0)));
center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
await tester.tapAt(Offset(center.dx, center.dy + 50.0));
await tester.tapAt(Offset(center.dx - 50, center.dy));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 9, minute: 0)));
});
testWidgets('drag-select an hour', (WidgetTester tester) async {
late TimeOfDay result;
final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
final Offset hour0 = Offset(center.dx, center.dy - 50.0); // 12:00 AM
final Offset hour3 = Offset(center.dx + 50.0, center.dy);
final Offset hour6 = Offset(center.dx, center.dy + 50.0);
final Offset hour9 = Offset(center.dx - 50.0, center.dy);
TestGesture gesture;
gesture = await tester.startGesture(hour3);
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(result.hour, 0);
expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
gesture = await tester.startGesture(hour0);
await gesture.moveBy(hour3 - hour0);
await gesture.up();
await finishPicker(tester);
expect(result.hour, 3);
expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
gesture = await tester.startGesture(hour3);
await gesture.moveBy(hour6 - hour3);
await gesture.up();
await finishPicker(tester);
expect(result.hour, equals(6));
expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
gesture = await tester.startGesture(hour6);
await gesture.moveBy(hour9 - hour6);
await gesture.up();
await finishPicker(tester);
expect(result.hour, equals(9));
});
testWidgets('tap-select switches from hour to minute', (WidgetTester tester) async {
late TimeOfDay result;
final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(min45);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
});
testWidgets('drag-select switches from hour to minute', (WidgetTester tester) async {
late TimeOfDay result;
final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
final Offset hour3 = Offset(center.dx + 50.0, center.dy);
final Offset hour6 = Offset(center.dx, center.dy + 50.0);
final Offset hour9 = Offset(center.dx - 50.0, center.dy);
TestGesture gesture = await tester.startGesture(hour6);
await gesture.moveBy(hour9 - hour6);
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
gesture = await tester.startGesture(hour6);
await gesture.moveBy(hour3 - hour6);
await gesture.up();
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
});
testWidgets('tap-select rounds down to nearest 5 minute increment', (WidgetTester tester) async {
late TimeOfDay result;
final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min46 = Offset(center.dx - 50.0, center.dy - 5); // 46 mins
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(min46);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
});
testWidgets('tap-select rounds up to nearest 5 minute increment', (WidgetTester tester) async {
late TimeOfDay result;
final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min48 = Offset(center.dx - 50.0, center.dy - 15); // 48 mins
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(min48);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 50)));
});
group('haptic feedback', () {
const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
late FeedbackTester feedback;
setUp(() {
feedback = FeedbackTester();
});
tearDown(() {
feedback.dispose();
});
testWidgets('tap-select vibrates once', (WidgetTester tester) async {
final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
expect(feedback.hapticCount, 1);
});
testWidgets('quick successive tap-selects vibrate once', (WidgetTester tester) async {
final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
await tester.pump(kFastFeedbackInterval);
await tester.tapAt(Offset(center.dx, center.dy + 50.0));
await finishPicker(tester);
expect(feedback.hapticCount, 1);
});
testWidgets('slow successive tap-selects vibrate once per tap', (WidgetTester tester) async {
final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
await tester.pump(kSlowFeedbackInterval);
await tester.tapAt(Offset(center.dx, center.dy + 50.0));
await tester.pump(kSlowFeedbackInterval);
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
expect(feedback.hapticCount, 3);
});
testWidgets('drag-select vibrates once', (WidgetTester tester) async {
final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
final Offset hour0 = Offset(center.dx, center.dy - 50.0);
final Offset hour3 = Offset(center.dx + 50.0, center.dy);
final TestGesture gesture = await tester.startGesture(hour3);
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(feedback.hapticCount, 1);
});
testWidgets('quick drag-select vibrates once', (WidgetTester tester) async {
final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
final Offset hour0 = Offset(center.dx, center.dy - 50.0);
final Offset hour3 = Offset(center.dx + 50.0, center.dy);
final TestGesture gesture = await tester.startGesture(hour3);
await gesture.moveBy(hour0 - hour3);
await tester.pump(kFastFeedbackInterval);
await gesture.moveBy(hour3 - hour0);
await tester.pump(kFastFeedbackInterval);
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(feedback.hapticCount, 1);
});
testWidgets('slow drag-select vibrates once', (WidgetTester tester) async {
final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
final Offset hour0 = Offset(center.dx, center.dy - 50.0);
final Offset hour3 = Offset(center.dx + 50.0, center.dy);
final TestGesture gesture = await tester.startGesture(hour3);
await gesture.moveBy(hour0 - hour3);
await tester.pump(kSlowFeedbackInterval);
await gesture.moveBy(hour3 - hour0);
await tester.pump(kSlowFeedbackInterval);
await gesture.moveBy(hour0 - hour3);
await gesture.up();
await finishPicker(tester);
expect(feedback.hapticCount, 3);
});
});
const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
const List<String> labels00To22 = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false);
final CustomPaint dialPaint = tester.widget(findDialPaint);
final dynamic dialPainter = dialPaint.painter;
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
});
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true);
final CustomPaint dialPaint = tester.widget(findDialPaint);
final dynamic dialPainter = dialPaint.painter;
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
});
testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, false);
expect(
semantics,
includesNodeWith(
label: 'AM',
actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isChecked,
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isFocusable,
],
),
);
expect(
semantics,
includesNodeWith(
label: 'PM',
actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isFocusable,
],
),
);
semantics.dispose();
});
testWidgets('provides semantics information for header and footer', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, true);
expect(semantics, isNot(includesNodeWith(label: ':')));
expect(semantics.nodesWith(value: 'Select minutes 00'), hasLength(1),
reason: '00 appears once in the header');
expect(semantics.nodesWith(value: 'Select hours 07'), hasLength(1),
reason: '07 appears once in the header');
expect(semantics, includesNodeWith(label: 'CANCEL'));
expect(semantics, includesNodeWith(label: 'OK'));
// In 24-hour mode we don't have AM/PM control.
expect(semantics, isNot(includesNodeWith(label: 'AM')));
expect(semantics, isNot(includesNodeWith(label: 'PM')));
semantics.dispose();
});
testWidgets('provides semantics information for text fields', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, accessibleNavigation: true);
expect(
semantics,
includesNodeWith(
label: 'Hour',
value: '07',
actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isMultiline],
),
);
expect(
semantics,
includesNodeWith(
label: 'Minute',
value: '00',
actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isMultiline],
),
);
semantics.dispose();
});
testWidgets('can increment and decrement hours', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
Future<void> actAndExpect({ required String initialValue, required SemanticsAction action, required String finalValue }) async {
final SemanticsNode elevenHours = semantics.nodesWith(
value: 'Select hours $initialValue',
ancestor: tester.renderObject(_hourControl).debugSemantics,
).single;
tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
await tester.pumpAndSettle();
expect(
find.descendant(of: _hourControl, matching: find.text(finalValue)),
findsOneWidget,
);
}
// 12-hour format
await mediaQueryBoilerplate(tester, false, initialTime: const TimeOfDay(hour: 11, minute: 0));
await actAndExpect(
initialValue: '11',
action: SemanticsAction.increase,
finalValue: '12',
);
await actAndExpect(
initialValue: '12',
action: SemanticsAction.increase,
finalValue: '1',
);
// Ensure we preserve day period as we roll over.
final dynamic pickerState = tester.state(_timePickerDialog); // ignore: unnecessary_nullable_for_final_variable_declarations
expect(pickerState.selectedTime, const TimeOfDay(hour: 1, minute: 0));
await actAndExpect(
initialValue: '1',
action: SemanticsAction.decrease,
finalValue: '12',
);
await tester.pumpWidget(Container()); // clear old boilerplate
// 24-hour format
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 23, minute: 0));
await actAndExpect(
initialValue: '23',
action: SemanticsAction.increase,
finalValue: '00',
);
await actAndExpect(
initialValue: '00',
action: SemanticsAction.increase,
finalValue: '01',
);
await actAndExpect(
initialValue: '01',
action: SemanticsAction.decrease,
finalValue: '00',
);
await actAndExpect(
initialValue: '00',
action: SemanticsAction.decrease,
finalValue: '23',
);
semantics.dispose();
});
testWidgets('can increment and decrement minutes', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
Future<void> actAndExpect({ required String initialValue, required SemanticsAction action, required String finalValue }) async {
final SemanticsNode elevenHours = semantics.nodesWith(
value: 'Select minutes $initialValue',
ancestor: tester.renderObject(_minuteControl).debugSemantics,
).single;
tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
await tester.pumpAndSettle();
expect(
find.descendant(of: _minuteControl, matching: find.text(finalValue)),
findsOneWidget,
);
}
await mediaQueryBoilerplate(tester, false, initialTime: const TimeOfDay(hour: 11, minute: 58));
await actAndExpect(
initialValue: '58',
action: SemanticsAction.increase,
finalValue: '59',
);
await actAndExpect(
initialValue: '59',
action: SemanticsAction.increase,
finalValue: '00',
);
// Ensure we preserve hour period as we roll over.
final dynamic pickerState = tester.state(_timePickerDialog); // ignore: unnecessary_nullable_for_final_variable_declarations
expect(pickerState.selectedTime, const TimeOfDay(hour: 11, minute: 0));
await actAndExpect(
initialValue: '00',
action: SemanticsAction.decrease,
finalValue: '59',
);
await actAndExpect(
initialValue: '59',
action: SemanticsAction.decrease,
finalValue: '58',
);
semantics.dispose();
});
testWidgets('header touch regions are large enough', (WidgetTester tester) async {
// Ensure picker is displayed in portrait mode.
tester.binding.window.physicalSizeTestValue = const Size(400, 800);
tester.binding.window.devicePixelRatioTestValue = 1;
await mediaQueryBoilerplate(tester, false);
final Size dayPeriodControlSize = tester.getSize(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'));
expect(dayPeriodControlSize.width, greaterThanOrEqualTo(48.0));
// Height should be double the minimum size to account for both AM/PM stacked.
expect(dayPeriodControlSize.height, greaterThanOrEqualTo(48.0 * 2));
final Size hourSize = tester.getSize(find.ancestor(
of: find.text('7'),
matching: find.byType(InkWell),
));
expect(hourSize.width, greaterThanOrEqualTo(48.0));
expect(hourSize.height, greaterThanOrEqualTo(48.0));
final Size minuteSize = tester.getSize(find.ancestor(
of: find.text('00'),
matching: find.byType(InkWell),
));
expect(minuteSize.width, greaterThanOrEqualTo(48.0));
expect(minuteSize.height, greaterThanOrEqualTo(48.0));
tester.binding.window.clearPhysicalSizeTestValue();
tester.binding.window.clearDevicePixelRatioTestValue();
});
testWidgets('builder parameter', (WidgetTester tester) async {
Widget buildFrame(TextDirection textDirection) {
return MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('X'),
onPressed: () {
showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
builder: (BuildContext context, Widget? child) {
return Directionality(
textDirection: textDirection,
child: child!,
);
},
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildFrame(TextDirection.ltr));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double ltrOkRight = tester.getBottomRight(find.text('OK')).dx;
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(TextDirection.rtl));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
// Verify that the time picker is being laid out RTL.
// We expect the left edge of the 'OK' button in the RTL
// layout to match the gap between right edge of the 'OK'
// button and the right edge of the 800 wide window.
expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight);
});
testWidgets('uses root navigator by default', (WidgetTester tester) async {
final PickerObserver rootObserver = PickerObserver();
final PickerObserver nestedObserver = PickerObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
);
},
child: const Text('Show Picker'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
expect(rootObserver.pickerCount, 1);
expect(nestedObserver.pickerCount, 0);
});
testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
final PickerObserver rootObserver = PickerObserver();
final PickerObserver nestedObserver = PickerObserver();
await tester.pumpWidget(MaterialApp(
navigatorObservers: <NavigatorObserver>[rootObserver],
home: Navigator(
observers: <NavigatorObserver>[nestedObserver],
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () {
showTimePicker(
context: context,
useRootNavigator: false,
initialTime: const TimeOfDay(hour: 7, minute: 0),
);
},
child: const Text('Show Picker'),
);
},
);
},
),
));
// Open the dialog.
await tester.tap(find.byType(ElevatedButton));
expect(rootObserver.pickerCount, 0);
expect(nestedObserver.pickerCount, 1);
});
testWidgets('optional text parameters are utilized', (WidgetTester tester) async {
const String cancelText = 'Custom Cancel';
const String confirmText = 'Custom OK';
const String helperText = 'Custom Help';
await tester.pumpWidget(MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('X'),
onPressed: () async {
await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
cancelText: cancelText,
confirmText: confirmText,
helpText: helperText,
);
},
);
}
),
),
)
));
// Open the picker.
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text(cancelText), findsOneWidget);
expect(find.text(confirmText), findsOneWidget);
expect(find.text(helperText), findsOneWidget);
});
testWidgets('OK Cancel button layout', (WidgetTester tester) async {
Widget buildFrame(TextDirection textDirection) {
return MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('X'),
onPressed: () {
showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
builder: (BuildContext context, Widget? child) {
return Directionality(
textDirection: textDirection,
child: child!,
);
},
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildFrame(TextDirection.ltr));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(tester.getBottomRight(find.text('OK')).dx, 638);
expect(tester.getBottomLeft(find.text('OK')).dx, 610);
expect(tester.getBottomRight(find.text('CANCEL')).dx, 576);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
await tester.pumpWidget(buildFrame(TextDirection.rtl));
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(tester.getBottomLeft(find.text('OK')).dx, 162);
expect(tester.getBottomRight(find.text('OK')).dx, 190);
expect(tester.getBottomLeft(find.text('CANCEL')).dx, 224);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
});
testWidgets('text scale affects certain elements and not others', (WidgetTester tester) async {
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 1.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
final double amHeight = tester.getSize(find.text('AM')).height;
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
// Verify that the time display is not affected by text scale.
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 2.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double amHeight2x = tester.getSize(find.text('AM')).height;
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(amHeight2x, greaterThanOrEqualTo(amHeight * 2));
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
// Verify that text scale for AM/PM is at most 2x.
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 3.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(tester.getSize(find.text('AM')).height, equals(amHeight2x));
});
}
void _testsInput() {
testWidgets('Initial entry mode is used', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
expect(find.byType(TextField), findsNWidgets(2));
});
testWidgets('Initial time is the default', (WidgetTester tester) async {
late TimeOfDay result;
await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 7, minute: 0)));
});
testWidgets('Help text is used - Input', (WidgetTester tester) async {
const String helpText = 'help';
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, helpText: helpText);
expect(find.text(helpText), findsOneWidget);
});
testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
await tester.tap(find.byIcon(Icons.access_time));
await tester.pumpAndSettle();
expect(find.byType(TextField), findsNothing);
});
testWidgets('Can double tap hours (when selected) to enter input mode', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
final Finder hourFinder = find.ancestor(
of: find.text('7'),
matching: find.byType(InkWell),
);
expect(find.byType(TextField), findsNothing);
// Double tap the hour.
await tester.tap(hourFinder);
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(hourFinder);
await tester.pumpAndSettle();
expect(find.byType(TextField), findsWidgets);
});
testWidgets('Can not double tap hours (when not selected) to enter input mode', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
final Finder hourFinder = find.ancestor(
of: find.text('7'),
matching: find.byType(InkWell),
);
final Finder minuteFinder = find.ancestor(
of: find.text('00'),
matching: find.byType(InkWell),
);
expect(find.byType(TextField), findsNothing);
// Switch to minutes mode.
await tester.tap(minuteFinder);
await tester.pumpAndSettle();
// Double tap the hour.
await tester.tap(hourFinder);
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(hourFinder);
await tester.pumpAndSettle();
expect(find.byType(TextField), findsNothing);
});
testWidgets('Can double tap minutes (when selected) to enter input mode', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
final Finder minuteFinder = find.ancestor(
of: find.text('00'),
matching: find.byType(InkWell),
);
expect(find.byType(TextField), findsNothing);
// Switch to minutes mode.
await tester.tap(minuteFinder);
await tester.pumpAndSettle();
// Double tap the minutes.
await tester.tap(minuteFinder);
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(minuteFinder);
await tester.pumpAndSettle();
expect(find.byType(TextField), findsWidgets);
});
testWidgets('Can not double tap minutes (when not selected) to enter input mode', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
final Finder minuteFinder = find.ancestor(
of: find.text('00'),
matching: find.byType(InkWell),
);
expect(find.byType(TextField), findsNothing);
// Double tap the minutes.
await tester.tap(minuteFinder);
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(minuteFinder);
await tester.pumpAndSettle();
expect(find.byType(TextField), findsNothing);
});
testWidgets('Entered text returns time', (WidgetTester tester) async {
late TimeOfDay result;
await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
await tester.enterText(find.byType(TextField).first, '9');
await tester.enterText(find.byType(TextField).last, '12');
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
});
testWidgets('Toggle to dial mode keeps selected time', (WidgetTester tester) async {
late TimeOfDay result;
await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
await tester.enterText(find.byType(TextField).first, '8');
await tester.enterText(find.byType(TextField).last, '15');
await tester.tap(find.byIcon(Icons.access_time));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
});
testWidgets('Invalid text prevents dismissing', (WidgetTester tester) async {
TimeOfDay? result;
await startPicker(tester, (TimeOfDay? time) { result = time; }, entryMode: TimePickerEntryMode.input);
// Invalid hour.
await tester.enterText(find.byType(TextField).first, '88');
await tester.enterText(find.byType(TextField).last, '15');
await finishPicker(tester);
expect(result, null);
// Invalid minute.
await tester.enterText(find.byType(TextField).first, '8');
await tester.enterText(find.byType(TextField).last, '95');
await finishPicker(tester);
expect(result, null);
await tester.enterText(find.byType(TextField).first, '8');
await tester.enterText(find.byType(TextField).last, '15');
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
});
// Fixes regression that was reverted in https://github.com/flutter/flutter/pull/64094#pullrequestreview-469836378.
testWidgets('Ensure hour/minute fields are top-aligned with the separator', (WidgetTester tester) async {
await startPicker(tester, (TimeOfDay? time) { }, entryMode: TimePickerEntryMode.input);
final double hourFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField')).dy;
final double minuteFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField')).dy;
final double separatorTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment')).dy;
expect(hourFieldTop, separatorTop);
expect(minuteFieldTop, separatorTop);
});
}
final Finder findDialPaint = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
);
class PickerObserver extends NavigatorObserver {
int pickerCount = 0;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
pickerCount++;
}
super.didPush(route, previousRoute);
}
}
Future<void> mediaQueryBoilerplate(
WidgetTester tester,
bool alwaysUse24HourFormat, {
TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
double textScaleFactor = 1.0,
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
String? helpText,
bool accessibleNavigation = false,
}) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MediaQuery(
data: MediaQueryData(
alwaysUse24HourFormat: alwaysUse24HourFormat,
textScaleFactor: textScaleFactor,
accessibleNavigation: accessibleNavigation,
),
child: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(builder: (BuildContext context) {
return TextButton(
onPressed: () {
showTimePicker(
context: context,
initialTime: initialTime,
initialEntryMode: entryMode,
helpText: helpText,
);
},
child: const Text('X'),
);
});
},
),
),
),
),
),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
}