Files
flutter-go/packages/flutter_web/test/material/time_picker_test.dart
2019-08-13 20:38:46 +08:00

699 lines
24 KiB
Dart

// Copyright 2016 The Chromium 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 'package:flutter_web_ui/ui.dart' as ui;
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/widgets.dart';
import 'package:flutter_web_test/flutter_web_test.dart';
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.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, this.onChanged, this.locale})
: super(key: key);
final ValueChanged<TimeOfDay> onChanged;
final Locale locale;
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: locale,
home: Material(
child: Center(child: Builder(builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () async {
onChanged(await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
));
});
}))));
}
}
Future<Offset> startPicker(
WidgetTester tester, ValueChanged<TimeOfDay> onChanged) async {
await tester.pumpWidget(_TimePickerLauncher(
onChanged: onChanged, locale: const Locale('en', 'US')));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
return tester
.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
}
Future<void> finishPicker(WidgetTester tester) async {
final MaterialLocalizations materialLocalizations =
MaterialLocalizations.of(tester.element(find.byType(RaisedButton)));
await tester.tap(find.text(materialLocalizations.okButtonLabel));
await tester.pumpAndSettle(const Duration(seconds: 1));
}
void main() {
group('Time picker', () {
_tests();
});
}
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 {
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));
});
group('haptic feedback', () {
const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
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> labels12To11TwoDigit = <String>[
'12',
'01',
'02',
'03',
'04',
'05',
'06',
'07',
'08',
'09',
'10',
'11'
];
const List<String> labels00To23 = <String>[
'00',
'13',
'14',
'15',
'16',
'17',
'18',
'19',
'20',
'21',
'22',
'23'
];
Future<void> mediaQueryBoilerplate(
WidgetTester tester, bool alwaysUse24HourFormat,
{TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0)}) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MediaQuery(
data: MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat),
child: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return FlatButton(
onPressed: () {
showTimePicker(
context: context, initialTime: initialTime);
},
child: const Text('X'),
);
});
},
),
),
),
),
),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
}
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> primaryOuterLabels = dialPainter.primaryOuterLabels;
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text),
labels12To11);
expect(dialPainter.primaryInnerLabels, null);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
expect(
secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text),
labels12To11);
expect(dialPainter.secondaryInnerLabels, null);
});
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> primaryOuterLabels = dialPainter.primaryOuterLabels;
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text),
labels00To23);
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels;
expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text),
labels12To11TwoDigit);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
expect(
secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text),
labels00To23);
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
expect(
secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text),
labels12To11TwoDigit);
});
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]));
expect(
semantics,
includesNodeWith(
label: 'PM', actions: <SemanticsAction>[SemanticsAction.tap]));
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: '00'), hasLength(2),
reason: '00 appears once in the header, then again in the dial');
expect(semantics.nodesWith(value: '07'), hasLength(2),
reason: '07 appears once in the header, then again in the dial');
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 hours',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, true);
final CustomPaint dialPaint =
tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final CustomPainter dialPainter = dialPaint.painter;
final _CustomPainterSemanticsTester painterTester =
_CustomPainterSemanticsTester(tester, dialPainter, semantics);
painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
painterTester.addLabel('13', 129.0, 11.5, 177.0, 59.5);
painterTester.addLabel('14', 160.5, 43.0, 208.5, 91.0);
painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
painterTester.addLabel('16', 160.5, 129.0, 208.5, 177.0);
painterTester.addLabel('17', 129.0, 160.5, 177.0, 208.5);
painterTester.addLabel('18', 86.0, 172.0, 134.0, 220.0);
painterTester.addLabel('19', 43.0, 160.5, 91.0, 208.5);
painterTester.addLabel('20', 11.5, 129.0, 59.5, 177.0);
painterTester.addLabel('21', 0.0, 86.0, 48.0, 134.0);
painterTester.addLabel('22', 11.5, 43.0, 59.5, 91.0);
painterTester.addLabel('23', 43.0, 11.5, 91.0, 59.5);
painterTester.addLabel('12', 86.0, 36.0, 134.0, 84.0);
painterTester.addLabel('01', 111.0, 42.7, 159.0, 90.7);
painterTester.addLabel('02', 129.3, 61.0, 177.3, 109.0);
painterTester.addLabel('03', 136.0, 86.0, 184.0, 134.0);
painterTester.addLabel('04', 129.3, 111.0, 177.3, 159.0);
painterTester.addLabel('05', 111.0, 129.3, 159.0, 177.3);
painterTester.addLabel('06', 86.0, 136.0, 134.0, 184.0);
painterTester.addLabel('07', 61.0, 129.3, 109.0, 177.3);
painterTester.addLabel('08', 42.7, 111.0, 90.7, 159.0);
painterTester.addLabel('09', 36.0, 86.0, 84.0, 134.0);
painterTester.addLabel('10', 42.7, 61.0, 90.7, 109.0);
painterTester.addLabel('11', 61.0, 42.7, 109.0, 90.7);
painterTester.assertExpectations();
semantics.dispose();
});
testWidgets('provides semantics information for minutes',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, true);
await tester.tap(_minuteControl);
await tester.pumpAndSettle();
final CustomPaint dialPaint =
tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final CustomPainter dialPainter = dialPaint.painter;
final _CustomPainterSemanticsTester painterTester =
_CustomPainterSemanticsTester(tester, dialPainter, semantics);
painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
painterTester.addLabel('05', 129.0, 11.5, 177.0, 59.5);
painterTester.addLabel('10', 160.5, 43.0, 208.5, 91.0);
painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
painterTester.addLabel('20', 160.5, 129.0, 208.5, 177.0);
painterTester.addLabel('25', 129.0, 160.5, 177.0, 208.5);
painterTester.addLabel('30', 86.0, 172.0, 134.0, 220.0);
painterTester.addLabel('35', 43.0, 160.5, 91.0, 208.5);
painterTester.addLabel('40', 11.5, 129.0, 59.5, 177.0);
painterTester.addLabel('45', 0.0, 86.0, 48.0, 134.0);
painterTester.addLabel('50', 11.5, 43.0, 59.5, 91.0);
painterTester.addLabel('55', 43.0, 11.5, 91.0, 59.5);
painterTester.assertExpectations();
semantics.dispose();
});
testWidgets('picks the right dial ring from widget configuration',
(WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true,
initialTime: const TimeOfDay(hour: 12, minute: 0));
dynamic dialPaint = tester.widget(findDialPaint);
expect('${dialPaint.painter.activeRing}', '_DialRing.inner');
await tester
.pumpWidget(Container()); // make sure previous state isn't reused
await mediaQueryBoilerplate(tester, true,
initialTime: const TimeOfDay(hour: 0, minute: 0));
dialPaint = tester.widget(findDialPaint);
expect('${dialPaint.painter.activeRing}', '_DialRing.outer');
});
testWidgets('can increment and decrement hours', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
Future<void> actAndExpect(
{String initialValue,
SemanticsAction action,
String finalValue}) async {
final SemanticsNode elevenHours = semantics
.nodesWith(
value: 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);
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(
{String initialValue,
SemanticsAction action,
String finalValue}) async {
final SemanticsNode elevenHours = semantics
.nodesWith(
value: 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);
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();
});
}
final Finder findDialPaint = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
);
class _SemanticsNodeExpectation {
_SemanticsNodeExpectation(
this.label, this.left, this.top, this.right, this.bottom);
final String label;
final double left;
final double top;
final double right;
final double bottom;
}
class _CustomPainterSemanticsTester {
_CustomPainterSemanticsTester(this.tester, this.painter, this.semantics);
final WidgetTester tester;
final CustomPainter painter;
final SemanticsTester semantics;
final PaintPattern expectedLabels = paints;
final List<_SemanticsNodeExpectation> expectedNodes =
<_SemanticsNodeExpectation>[];
void addLabel(
String label, double left, double top, double right, double bottom) {
expectedNodes
.add(_SemanticsNodeExpectation(label, left, top, right, bottom));
}
void assertExpectations() {
final TestRecordingCanvas canvasRecording = TestRecordingCanvas();
painter.paint(canvasRecording, const Size(220.0, 220.0));
final List<ui.Paragraph> paragraphs = canvasRecording.invocations
.where((RecordedInvocation recordedInvocation) {
return recordedInvocation.invocation.memberName == #drawParagraph;
}).map<ui.Paragraph>((RecordedInvocation recordedInvocation) {
return recordedInvocation.invocation.positionalArguments.first;
}).toList();
final PaintPattern expectedLabels = paints;
int i = 0;
for (_SemanticsNodeExpectation expectation in expectedNodes) {
expect(semantics, includesNodeWith(value: expectation.label));
final Iterable<SemanticsNode> dialLabelNodes = semantics
.nodesWith(value: expectation.label)
.where((SemanticsNode node) =>
node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
expect(dialLabelNodes, hasLength(1),
reason: 'Expected exactly one label ${expectation.label}');
final Rect rect = Rect.fromLTRB(expectation.left, expectation.top,
expectation.right, expectation.bottom);
expect(dialLabelNodes.single.rect, within(distance: 1.0, from: rect),
reason:
'This is checking the node rectangle for label ${expectation.label}');
final ui.Paragraph paragraph = paragraphs[i++];
// The label text paragraph and the semantics node share the same center,
// but have different sizes.
final Offset center = dialLabelNodes.single.rect.center;
final Offset topLeft = center.translate(
-paragraph.width / 2.0,
-paragraph.height / 2.0,
);
expectedLabels.paragraph(
paragraph: paragraph,
offset: within<Offset>(distance: 1.0, from: topLeft),
);
}
expect(tester.renderObject(findDialPaint), expectedLabels);
}
}