make date picker accessible (#13502)
* make date picker accessible * make test file lookup location-independent * address some comments * always wrap in IgnorePointer * no bitmasks for flags and actions * recommend List<*>
This commit is contained in:
parent
dc9c95375f
commit
235b64ed2f
@ -126,22 +126,31 @@ class _DatePickerHeader extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
|
||||
Widget yearButton = new _DateHeaderButton(
|
||||
color: backgroundColor,
|
||||
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
|
||||
child: new Text(localizations.formatYear(selectedDate), style: yearStyle),
|
||||
);
|
||||
Widget dayButton = new _DateHeaderButton(
|
||||
color: backgroundColor,
|
||||
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
|
||||
child: new Text(localizations.formatMediumDate(selectedDate), style: dayStyle),
|
||||
final Widget yearButton = new IgnorePointer(
|
||||
ignoring: mode != DatePickerMode.day,
|
||||
ignoringSemantics: false,
|
||||
child: new _DateHeaderButton(
|
||||
color: backgroundColor,
|
||||
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
|
||||
child: new Semantics(
|
||||
selected: mode == DatePickerMode.year,
|
||||
child: new Text(localizations.formatYear(selectedDate), style: yearStyle),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Disable the button for the current mode.
|
||||
if (mode == DatePickerMode.day)
|
||||
dayButton = new IgnorePointer(child: dayButton);
|
||||
else
|
||||
yearButton = new IgnorePointer(child: yearButton);
|
||||
final Widget dayButton = new IgnorePointer(
|
||||
ignoring: mode == DatePickerMode.day,
|
||||
ignoringSemantics: false,
|
||||
child: new _DateHeaderButton(
|
||||
color: backgroundColor,
|
||||
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
|
||||
child: new Semantics(
|
||||
selected: mode == DatePickerMode.day,
|
||||
child: new Text(localizations.formatMediumDate(selectedDate), style: dayStyle),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return new Container(
|
||||
width: width,
|
||||
@ -238,7 +247,6 @@ class DayPicker extends StatelessWidget {
|
||||
@required this.firstDate,
|
||||
@required this.lastDate,
|
||||
@required this.displayedMonth,
|
||||
this.onMonthHeaderTap,
|
||||
this.selectableDayPredicate,
|
||||
}) : assert(selectedDate != null),
|
||||
assert(currentDate != null),
|
||||
@ -259,9 +267,6 @@ class DayPicker extends StatelessWidget {
|
||||
/// Called when the user picks a day.
|
||||
final ValueChanged<DateTime> onChanged;
|
||||
|
||||
/// Called when the user taps on the header that displays the current month.
|
||||
final VoidCallback onMonthHeaderTap;
|
||||
|
||||
/// The earliest date the user is permitted to pick.
|
||||
final DateTime firstDate;
|
||||
|
||||
@ -296,7 +301,9 @@ class DayPicker extends StatelessWidget {
|
||||
final List<Widget> result = <Widget>[];
|
||||
for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
|
||||
final String weekday = localizations.narrowWeekdays[i];
|
||||
result.add(new Center(child: new Text(weekday, style: headerStyle)));
|
||||
result.add(new ExcludeSemantics(
|
||||
child: new Center(child: new Text(weekday, style: headerStyle)),
|
||||
));
|
||||
if (i == (localizations.firstDayOfWeekIndex - 1) % 7)
|
||||
break;
|
||||
}
|
||||
@ -392,7 +399,8 @@ class DayPicker extends StatelessWidget {
|
||||
BoxDecoration decoration;
|
||||
TextStyle itemStyle = themeData.textTheme.body1;
|
||||
|
||||
if (selectedDate.year == year && selectedDate.month == month && selectedDate.day == day) {
|
||||
final bool isSelectedDay = selectedDate.year == year && selectedDate.month == month && selectedDate.day == day;
|
||||
if (isSelectedDay) {
|
||||
// The selected day gets a circle background highlight, and a contrasting text color.
|
||||
itemStyle = themeData.accentTextTheme.body2;
|
||||
decoration = new BoxDecoration(
|
||||
@ -409,7 +417,19 @@ class DayPicker extends StatelessWidget {
|
||||
Widget dayWidget = new Container(
|
||||
decoration: decoration,
|
||||
child: new Center(
|
||||
child: new Text(localizations.formatDecimal(day), style: itemStyle),
|
||||
child: new Semantics(
|
||||
// We want the day of month to be spoken first irrespective of the
|
||||
// locale-specific preferences or TextDirection. This is because
|
||||
// an accessibility user is more likely to be interested in the
|
||||
// day of month before the rest of the date, as they are looking
|
||||
// for the day of month. To do that we prepend day of month to the
|
||||
// formatted full date.
|
||||
label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}',
|
||||
selected: isSelectedDay,
|
||||
child: new ExcludeSemantics(
|
||||
child: new Text(localizations.formatDecimal(day), style: itemStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -434,9 +454,9 @@ class DayPicker extends StatelessWidget {
|
||||
new Container(
|
||||
height: _kDayPickerRowHeight,
|
||||
child: new Center(
|
||||
child: new GestureDetector(
|
||||
onTap: onMonthHeaderTap != null ? Feedback.wrapForTap(onMonthHeaderTap, context) : null,
|
||||
child: new Text(localizations.formatMonthYear(displayedMonth),
|
||||
child: new ExcludeSemantics(
|
||||
child: new Text(
|
||||
localizations.formatMonthYear(displayedMonth),
|
||||
style: themeData.textTheme.subhead,
|
||||
),
|
||||
),
|
||||
@ -478,7 +498,6 @@ class MonthPicker extends StatefulWidget {
|
||||
@required this.firstDate,
|
||||
@required this.lastDate,
|
||||
this.selectableDayPredicate,
|
||||
this.onMonthHeaderTap,
|
||||
}) : assert(selectedDate != null),
|
||||
assert(onChanged != null),
|
||||
assert(!firstDate.isAfter(lastDate)),
|
||||
@ -493,9 +512,6 @@ class MonthPicker extends StatefulWidget {
|
||||
/// Called when the user picks a month.
|
||||
final ValueChanged<DateTime> onChanged;
|
||||
|
||||
/// Called when the user taps on the header that displays the current month.
|
||||
final VoidCallback onMonthHeaderTap;
|
||||
|
||||
/// The earliest date the user is permitted to pick.
|
||||
final DateTime firstDate;
|
||||
|
||||
@ -514,8 +530,9 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initially display the pre-selected date.
|
||||
_dayPickerController = new PageController(initialPage: _monthDelta(widget.firstDate, widget.selectedDate));
|
||||
_currentDisplayedMonthDate = new DateTime(widget.selectedDate.year, widget.selectedDate.month);
|
||||
final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate);
|
||||
_dayPickerController = new PageController(initialPage: monthPage);
|
||||
_handleMonthPageChanged(monthPage);
|
||||
_updateCurrentDate();
|
||||
}
|
||||
|
||||
@ -523,12 +540,22 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
void didUpdateWidget(MonthPicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedDate != oldWidget.selectedDate) {
|
||||
_dayPickerController = new PageController(initialPage: _monthDelta(widget.firstDate, widget.selectedDate));
|
||||
_currentDisplayedMonthDate =
|
||||
new DateTime(widget.selectedDate.year, widget.selectedDate.month);
|
||||
final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate);
|
||||
_dayPickerController = new PageController(initialPage: monthPage);
|
||||
_handleMonthPageChanged(monthPage);
|
||||
}
|
||||
}
|
||||
|
||||
MaterialLocalizations localizations;
|
||||
TextDirection textDirection;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
localizations = MaterialLocalizations.of(context);
|
||||
textDirection = Directionality.of(context);
|
||||
}
|
||||
|
||||
DateTime _todayDate;
|
||||
DateTime _currentDisplayedMonthDate;
|
||||
Timer _timer;
|
||||
@ -567,18 +594,21 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
lastDate: widget.lastDate,
|
||||
displayedMonth: month,
|
||||
selectableDayPredicate: widget.selectableDayPredicate,
|
||||
onMonthHeaderTap: widget.onMonthHeaderTap,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleNextMonth() {
|
||||
if (!_isDisplayingLastMonth)
|
||||
if (!_isDisplayingLastMonth) {
|
||||
SemanticsService.announce(localizations.formatMonthYear(_nextMonthDate), textDirection);
|
||||
_dayPickerController.nextPage(duration: _kMonthScrollDuration, curve: Curves.ease);
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePreviousMonth() {
|
||||
if (!_isDisplayingFirstMonth)
|
||||
if (!_isDisplayingFirstMonth) {
|
||||
SemanticsService.announce(localizations.formatMonthYear(_previousMonthDate), textDirection);
|
||||
_dayPickerController.previousPage(duration: _kMonthScrollDuration, curve: Curves.ease);
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the earliest allowable month is displayed.
|
||||
@ -593,15 +623,19 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
new DateTime(widget.lastDate.year, widget.lastDate.month));
|
||||
}
|
||||
|
||||
DateTime _previousMonthDate;
|
||||
DateTime _nextMonthDate;
|
||||
|
||||
void _handleMonthPageChanged(int monthPage) {
|
||||
setState(() {
|
||||
_previousMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage - 1);
|
||||
_currentDisplayedMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage);
|
||||
_nextMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
return new SizedBox(
|
||||
width: _kMonthPickerPortraitWidth,
|
||||
height: _kMaxDayPickerHeight,
|
||||
@ -620,7 +654,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
start: 8.0,
|
||||
child: new IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
tooltip: localizations.previousMonthTooltip,
|
||||
tooltip: _isDisplayingFirstMonth ? null : '${localizations.previousMonthTooltip} ${localizations.formatMonthYear(_previousMonthDate)}',
|
||||
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
|
||||
),
|
||||
),
|
||||
@ -629,7 +663,7 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
end: 8.0,
|
||||
child: new IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
tooltip: localizations.nextMonthTooltip,
|
||||
tooltip: _isDisplayingLastMonth ? null : '${localizations.nextMonthTooltip} ${localizations.formatMonthYear(_nextMonthDate)}',
|
||||
onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
|
||||
),
|
||||
),
|
||||
@ -640,8 +674,8 @@ class _MonthPickerState extends State<MonthPicker> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_timer != null)
|
||||
_timer.cancel();
|
||||
_timer?.cancel();
|
||||
_dayPickerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -718,15 +752,20 @@ class _YearPickerState extends State<YearPicker> {
|
||||
itemCount: widget.lastDate.year - widget.firstDate.year + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final int year = widget.firstDate.year + index;
|
||||
final TextStyle itemStyle = year == widget.selectedDate.year ?
|
||||
themeData.textTheme.headline.copyWith(color: themeData.accentColor) : style;
|
||||
final bool isSelected = year == widget.selectedDate.year;
|
||||
final TextStyle itemStyle = isSelected
|
||||
? themeData.textTheme.headline.copyWith(color: themeData.accentColor)
|
||||
: style;
|
||||
return new InkWell(
|
||||
key: new ValueKey<int>(year),
|
||||
onTap: () {
|
||||
widget.onChanged(new DateTime(year, widget.selectedDate.month, widget.selectedDate.day));
|
||||
},
|
||||
child: new Center(
|
||||
child: new Text(year.toString(), style: itemStyle),
|
||||
child: new Semantics(
|
||||
selected: isSelected,
|
||||
child: new Text(year.toString(), style: itemStyle),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -762,6 +801,25 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
|
||||
_mode = widget.initialDatePickerMode;
|
||||
}
|
||||
|
||||
bool _announcedInitialDate = false;
|
||||
|
||||
MaterialLocalizations localizations;
|
||||
TextDirection textDirection;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
localizations = MaterialLocalizations.of(context);
|
||||
textDirection = Directionality.of(context);
|
||||
if (!_announcedInitialDate) {
|
||||
_announcedInitialDate = true;
|
||||
SemanticsService.announce(
|
||||
localizations.formatFullDate(_selectedDate),
|
||||
textDirection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _selectedDate;
|
||||
DatePickerMode _mode;
|
||||
final GlobalKey _pickerKey = new GlobalKey();
|
||||
@ -781,6 +839,11 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
|
||||
_vibrate();
|
||||
setState(() {
|
||||
_mode = mode;
|
||||
if (_mode == DatePickerMode.day) {
|
||||
SemanticsService.announce(localizations.formatMonthYear(_selectedDate), textDirection);
|
||||
} else {
|
||||
SemanticsService.announce(localizations.formatYear(_selectedDate), textDirection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -807,10 +870,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
|
||||
Navigator.pop(context, _selectedDate);
|
||||
}
|
||||
|
||||
void _handleMonthHeaderTap() {
|
||||
_handleModeChanged(DatePickerMode.year);
|
||||
}
|
||||
|
||||
Widget _buildPicker() {
|
||||
assert(_mode != null);
|
||||
switch (_mode) {
|
||||
@ -822,7 +881,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
|
||||
firstDate: widget.firstDate,
|
||||
lastDate: widget.lastDate,
|
||||
selectableDayPredicate: widget.selectableDayPredicate,
|
||||
onMonthHeaderTap: _handleMonthHeaderTap,
|
||||
);
|
||||
case DatePickerMode.year:
|
||||
return new YearPicker(
|
||||
@ -844,7 +902,6 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
|
||||
child: _buildPicker(),
|
||||
),
|
||||
);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
final Widget actions = new ButtonTheme.bar(
|
||||
child: new ButtonBar(
|
||||
children: <Widget>[
|
||||
@ -862,13 +919,13 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
|
||||
return new Dialog(
|
||||
child: new OrientationBuilder(
|
||||
builder: (BuildContext context, Orientation orientation) {
|
||||
assert(orientation != null);
|
||||
final Widget header = new _DatePickerHeader(
|
||||
selectedDate: _selectedDate,
|
||||
mode: _mode,
|
||||
onModeChanged: _handleModeChanged,
|
||||
orientation: orientation,
|
||||
);
|
||||
assert(orientation != null);
|
||||
switch (orientation) {
|
||||
case Orientation.portrait:
|
||||
return new SizedBox(
|
||||
|
@ -196,6 +196,17 @@ abstract class MaterialLocalizations {
|
||||
/// - Russian: ср, сент. 27
|
||||
String formatMediumDate(DateTime date);
|
||||
|
||||
/// Formats day of week, month, day of month and year in a long-width format.
|
||||
///
|
||||
/// Does not abbreviate names. Appears in spoken announcements of the date
|
||||
/// picker invoked using [showDatePicker], when accessibility mode is on.
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - US English: Wednesday, September 27, 2017
|
||||
/// - Russian: Среда, Сентябрь 27, 2017
|
||||
String formatFullDate(DateTime date);
|
||||
|
||||
/// Formats the month and the year of the given [date].
|
||||
///
|
||||
/// The returned string does not contain the day of the month. This appears
|
||||
@ -275,7 +286,7 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
||||
const DefaultMaterialLocalizations();
|
||||
|
||||
// Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6
|
||||
static const List<String>_shortWeekdays = const <String>[
|
||||
static const List<String> _shortWeekdays = const <String>[
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
@ -285,6 +296,17 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
||||
'Sun',
|
||||
];
|
||||
|
||||
// Ordered to match DateTime.MONDAY=1, DateTime.SUNDAY=6
|
||||
static const List<String> _weekdays = const <String>[
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
];
|
||||
|
||||
static const List<String> _narrowWeekdays = const <String>[
|
||||
'S',
|
||||
'M',
|
||||
@ -365,6 +387,12 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
|
||||
return '$day, $month ${date.day}';
|
||||
}
|
||||
|
||||
@override
|
||||
String formatFullDate(DateTime date) {
|
||||
final String month = _months[date.month - DateTime.JANUARY];
|
||||
return '${_weekdays[date.weekday - DateTime.MONDAY]}, $month ${date.day}, ${date.year}';
|
||||
}
|
||||
|
||||
@override
|
||||
String formatMonthYear(DateTime date) {
|
||||
final String year = formatYear(date);
|
||||
|
@ -2,17 +2,28 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
import 'feedback_tester.dart';
|
||||
|
||||
void main() {
|
||||
group('showDatePicker', () {
|
||||
_tests();
|
||||
});
|
||||
}
|
||||
|
||||
void _tests() {
|
||||
DateTime firstDate;
|
||||
DateTime lastDate;
|
||||
DateTime initialDate;
|
||||
SelectableDayPredicate selectableDayPredicate;
|
||||
DatePickerMode initialDatePickerMode;
|
||||
final Finder nextMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Next month') ?? false));
|
||||
final Finder previousMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Previous month') ?? false));
|
||||
|
||||
setUp(() {
|
||||
firstDate = new DateTime(2001, DateTime.JANUARY, 1);
|
||||
@ -63,7 +74,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
|
||||
|
||||
await tester.tap(find.byTooltip('Next month'));
|
||||
await tester.tap(nextMonthIcon);
|
||||
await tester.pumpAndSettle();
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
|
||||
|
||||
@ -114,38 +125,6 @@ void main() {
|
||||
await tester.pump(const Duration(seconds: 5));
|
||||
});
|
||||
|
||||
testWidgets('MonthPicker receives header taps', (WidgetTester tester) async {
|
||||
DateTime currentValue;
|
||||
bool headerTapped = false;
|
||||
|
||||
final Widget widget = new MaterialApp(
|
||||
home: new Material(
|
||||
child: new ListView(
|
||||
children: <Widget>[
|
||||
new MonthPicker(
|
||||
selectedDate: new DateTime.utc(2015, 6, 9, 7, 12),
|
||||
firstDate: new DateTime.utc(2013),
|
||||
lastDate: new DateTime.utc(2018),
|
||||
onChanged: (DateTime dateTime) {
|
||||
currentValue = dateTime;
|
||||
},
|
||||
onMonthHeaderTap: () {
|
||||
headerTapped = true;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(widget);
|
||||
|
||||
expect(currentValue, isNull);
|
||||
expect(headerTapped, false);
|
||||
await tester.tap(find.text('June 2015'));
|
||||
expect(headerTapped, true);
|
||||
});
|
||||
|
||||
Future<Null> preparePicker(WidgetTester tester, Future<Null> callback(Future<DateTime> date)) async {
|
||||
BuildContext buttonContext;
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
@ -214,7 +193,7 @@ void main() {
|
||||
|
||||
testWidgets('Can select a month', (WidgetTester tester) async {
|
||||
await preparePicker(tester, (Future<DateTime> date) async {
|
||||
await tester.tap(find.byTooltip('Previous month'));
|
||||
await tester.tap(previousMonthIcon);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.tap(find.text('25'));
|
||||
await tester.tap(find.text('OK'));
|
||||
@ -279,17 +258,10 @@ void main() {
|
||||
firstDate = initialDate;
|
||||
lastDate = new DateTime(2017, DateTime.FEBRUARY, 20);
|
||||
await preparePicker(tester, (Future<DateTime> date) async {
|
||||
await tester.tap(find.byTooltip('Next month'));
|
||||
await tester.tap(nextMonthIcon);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
// Shouldn't be possible to keep going into March.
|
||||
await tester.tap(find.byTooltip('Next month'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
// We're still in February
|
||||
await tester.tap(find.text('20'));
|
||||
// Days outside bound for new month pages also disabled.
|
||||
await tester.tap(find.text('25'));
|
||||
await tester.tap(find.text('OK'));
|
||||
expect(await date, equals(new DateTime(2017, DateTime.FEBRUARY, 20)));
|
||||
expect(nextMonthIcon, findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
@ -298,17 +270,10 @@ void main() {
|
||||
firstDate = new DateTime(2016, DateTime.DECEMBER, 10);
|
||||
lastDate = initialDate;
|
||||
await preparePicker(tester, (Future<DateTime> date) async {
|
||||
await tester.tap(find.byTooltip('Previous month'));
|
||||
await tester.tap(previousMonthIcon);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
// Shouldn't be possible to keep going into November.
|
||||
await tester.tap(find.byTooltip('Previous month'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
// We're still in December
|
||||
await tester.tap(find.text('10'));
|
||||
// Days outside bound for new month pages also disabled.
|
||||
await tester.tap(find.text('5'));
|
||||
await tester.tap(find.text('OK'));
|
||||
expect(await date, equals(new DateTime(2016, DateTime.DECEMBER, 10)));
|
||||
expect(previousMonthIcon, findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
@ -417,4 +382,227 @@ void main() {
|
||||
expect(await date, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('exports semantics', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
await preparePicker(tester, (Future<DateTime> date) async {
|
||||
final TestSemantics expected = new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
flags: <SemanticsFlags>[SemanticsFlags.isSelected],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'Fri, Jan 15',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'1, Friday, January 1, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'2, Saturday, January 2, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'3, Sunday, January 3, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'4, Monday, January 4, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'5, Tuesday, January 5, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'6, Wednesday, January 6, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'7, Thursday, January 7, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'8, Friday, January 8, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'9, Saturday, January 9, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'10, Sunday, January 10, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'11, Monday, January 11, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'12, Tuesday, January 12, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'13, Wednesday, January 13, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'14, Thursday, January 14, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
flags: <SemanticsFlags>[SemanticsFlags.isSelected],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'15, Friday, January 15, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'16, Saturday, January 16, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'17, Sunday, January 17, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'18, Monday, January 18, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'19, Tuesday, January 19, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'20, Wednesday, January 20, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'21, Thursday, January 21, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'22, Friday, January 22, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'23, Saturday, January 23, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'24, Sunday, January 24, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'25, Monday, January 25, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'26, Tuesday, January 26, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'27, Wednesday, January 27, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'28, Thursday, January 28, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'29, Friday, January 29, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'30, Saturday, January 30, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'31, Sunday, January 31, 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'Previous month December 2015',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'Next month February 2016',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
flags: <SemanticsFlags>[SemanticsFlags.isButton],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'CANCEL',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
flags: <SemanticsFlags>[SemanticsFlags.isButton],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: r'OK',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(semantics, hasSemantics(
|
||||
expected,
|
||||
ignoreId: true,
|
||||
ignoreTransform: true,
|
||||
ignoreRect: true,
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -46,7 +46,8 @@ class TestSemantics {
|
||||
this.transform,
|
||||
this.children: const <TestSemantics>[],
|
||||
Iterable<SemanticsTag> tags,
|
||||
}) : assert(flags != null),
|
||||
}) : assert(flags is int || flags is List<SemanticsFlags>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
assert(label != null),
|
||||
assert(value != null),
|
||||
assert(increasedValue != null),
|
||||
@ -70,7 +71,8 @@ class TestSemantics {
|
||||
this.children: const <TestSemantics>[],
|
||||
Iterable<SemanticsTag> tags,
|
||||
}) : id = 0,
|
||||
assert(flags != null),
|
||||
assert(flags is int || flags is List<SemanticsFlags>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
assert(label != null),
|
||||
assert(increasedValue != null),
|
||||
assert(decreasedValue != null),
|
||||
@ -103,7 +105,8 @@ class TestSemantics {
|
||||
Matrix4 transform,
|
||||
this.children: const <TestSemantics>[],
|
||||
Iterable<SemanticsTag> tags,
|
||||
}) : assert(flags != null),
|
||||
}) : assert(flags is int || flags is List<SemanticsFlags>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
assert(label != null),
|
||||
assert(value != null),
|
||||
assert(increasedValue != null),
|
||||
@ -119,11 +122,24 @@ class TestSemantics {
|
||||
/// they are created.
|
||||
final int id;
|
||||
|
||||
/// A bit field of [SemanticsFlags] that apply to this node.
|
||||
final int flags;
|
||||
/// The [SemanticsFlags] set on this node.
|
||||
///
|
||||
/// There are two ways to specify this property: as an `int` that encodes the
|
||||
/// flags as a bit field, or as a `List<SemanticsFlags>` that are _on_.
|
||||
///
|
||||
/// Using `List<SemanticsFlags>` is recommended due to better readability.
|
||||
final dynamic flags;
|
||||
|
||||
/// A bit field of [SemanticsActions] that apply to this node.
|
||||
final int actions;
|
||||
/// The [SemanticsAction]s set on this node.
|
||||
///
|
||||
/// There are two ways to specify this property: as an `int` that encodes the
|
||||
/// actions as a bit field, or as a `List<SemanticsAction>`.
|
||||
///
|
||||
/// Using `List<SemanticsAction>` is recommended due to better readability.
|
||||
///
|
||||
/// The tester does not check the function corresponding to the action, but
|
||||
/// only its existence.
|
||||
final dynamic actions;
|
||||
|
||||
/// A textual description of this node.
|
||||
final String label;
|
||||
@ -204,10 +220,19 @@ class TestSemantics {
|
||||
return fail('could not find node with id $id.');
|
||||
if (!ignoreId && id != node.id)
|
||||
return fail('expected node id $id but found id ${node.id}.');
|
||||
if (flags != nodeData.flags)
|
||||
|
||||
final int flagsBitmask = flags is int
|
||||
? flags
|
||||
: flags.fold<int>(0, (int bitmask, SemanticsFlags flag) => bitmask | flag.index);
|
||||
if (flagsBitmask != nodeData.flags)
|
||||
return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.');
|
||||
if (actions != nodeData.actions)
|
||||
|
||||
final int actionsBitmask = actions is int
|
||||
? actions
|
||||
: actions.fold<int>(0, (int bitmask, SemanticsAction action) => bitmask | action.index);
|
||||
if (actionsBitmask != nodeData.actions)
|
||||
return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
|
||||
|
||||
if (label != nodeData.label)
|
||||
return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
|
||||
if (value != nodeData.value)
|
||||
@ -340,6 +365,109 @@ class SemanticsTester {
|
||||
visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generates an expression that creates a [TestSemantics] reflecting the
|
||||
/// current tree of [SemanticsNode]s.
|
||||
///
|
||||
/// Use this method to generate code for unit tests. It works similar to
|
||||
/// screenshot testing. The very first time you add semantics to a widget you
|
||||
/// verify manually that the widget behaves correctly. You then use ths method
|
||||
/// to generate test code for this widget.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```dart
|
||||
/// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
|
||||
/// var semantics = new SemanticsTester(tester);
|
||||
/// await tester.pumpWidget(new MyWidget());
|
||||
/// print(semantics.generateTestSemanticsExpressionForCurrentSemanticsTree());
|
||||
/// semantics.dispose();
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// You can now copy the code printed to the console into a unit test:
|
||||
///
|
||||
/// ```dart
|
||||
/// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
|
||||
/// var semantics = new SemanticsTester(tester);
|
||||
/// await tester.pumpWidget(new MyWidget());
|
||||
/// expect(semantics, hasSemantics(
|
||||
/// // Generated code:
|
||||
/// new TestSemantics(
|
||||
/// ... properties and child nodes ...
|
||||
/// ),
|
||||
/// ignoreRect: true,
|
||||
/// ignoreTransform: true,
|
||||
/// ignoreId: true,
|
||||
/// ));
|
||||
/// semantics.dispose();
|
||||
/// });
|
||||
///
|
||||
/// At this point the unit test should automatically pass because it was
|
||||
/// generated from the actual [SemanticsNode]s. Next time the semantics tree
|
||||
/// changes, the test code may either be updated manually, or regenerated and
|
||||
/// replaced using this method again.
|
||||
///
|
||||
/// Avoid submitting huge piles of generated test code. This will make test
|
||||
/// code hard to review and it will make it tempting to regenerate test code
|
||||
/// every time and ignore potential regressions. Make sure you do not
|
||||
/// over-test. Prefer breaking your widgets into smaller widgets and test them
|
||||
/// individually.
|
||||
String generateTestSemanticsExpressionForCurrentSemanticsTree() {
|
||||
final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
|
||||
return _generateSemanticsTestForNode(node, 0);
|
||||
}
|
||||
|
||||
String _flagsToSemanticsFlagsExpression(int bitmap) {
|
||||
return SemanticsFlags.values.values
|
||||
.where((SemanticsFlags flag) => (flag.index & bitmap) != 0)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
String _actionsToSemanticsActionExpression(int bitmap) {
|
||||
return SemanticsAction.values.values
|
||||
.where((SemanticsAction action) => (action.index & bitmap) != 0)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/// Recursively generates [TestSemantics] code for [node] and its children,
|
||||
/// indenting the expression by `indentAmount`.
|
||||
String _generateSemanticsTestForNode(SemanticsNode node, int indentAmount) {
|
||||
final String indent = ' ' * indentAmount;
|
||||
final StringBuffer buf = new StringBuffer();
|
||||
final SemanticsData nodeData = node.getSemanticsData();
|
||||
buf.writeln('new TestSemantics(');
|
||||
if (nodeData.flags != 0)
|
||||
buf.writeln(' flags: <SemanticsFlags>[${_flagsToSemanticsFlagsExpression(nodeData.flags)}],');
|
||||
if (nodeData.actions != 0)
|
||||
buf.writeln(' actions: <SemanticsAction>[${_actionsToSemanticsActionExpression(nodeData.actions)}],');
|
||||
if (node.label != null && node.label.isNotEmpty)
|
||||
buf.writeln(' label: r\'${node.label}\',');
|
||||
if (node.value != null && node.value.isNotEmpty)
|
||||
buf.writeln(' value: r\'${node.value}\',');
|
||||
if (node.increasedValue != null && node.increasedValue.isNotEmpty)
|
||||
buf.writeln(' increasedValue: r\'${node.increasedValue}\',');
|
||||
if (node.decreasedValue != null && node.decreasedValue.isNotEmpty)
|
||||
buf.writeln(' decreasedValue: r\'${node.decreasedValue}\',');
|
||||
if (node.hint != null && node.hint.isNotEmpty)
|
||||
buf.writeln(' hint: r\'${node.hint}\',');
|
||||
if (node.textDirection != null)
|
||||
buf.writeln(' textDirection: ${node.textDirection},');
|
||||
|
||||
if (node.hasChildren) {
|
||||
buf.writeln(' children: <TestSemantics>[');
|
||||
node.visitChildren((SemanticsNode child) {
|
||||
buf
|
||||
..write(_generateSemanticsTestForNode(child, 2))
|
||||
..writeln(',');
|
||||
return true;
|
||||
});
|
||||
buf.writeln(' ],');
|
||||
}
|
||||
|
||||
buf.write(')');
|
||||
return buf.toString().split('\n').map((String l) => '$indent$l').join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
class _HasSemantics extends Matcher {
|
||||
|
@ -0,0 +1,139 @@
|
||||
// Copyright 2017 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 'dart:io';
|
||||
import 'dart:ui' show SemanticsFlags;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
group('generateTestSemanticsExpressionForCurrentSemanticsTree', () {
|
||||
_tests();
|
||||
});
|
||||
}
|
||||
|
||||
void _tests() {
|
||||
setUp(() {
|
||||
debugResetSemanticsIdCounter();
|
||||
});
|
||||
|
||||
Future<Null> pumpTestWidget(WidgetTester tester) async {
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
home: new ListView(
|
||||
children: <Widget>[
|
||||
const Text('Plain text'),
|
||||
new Semantics(
|
||||
selected: true,
|
||||
checked: true,
|
||||
onTap: () {},
|
||||
onDecrease: () {},
|
||||
value: 'test-value',
|
||||
increasedValue: 'test-increasedValue',
|
||||
decreasedValue: 'test-decreasedValue',
|
||||
hint: 'test-hint',
|
||||
textDirection: TextDirection.rtl,
|
||||
child: const Text('Interactive text'),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// This test generates code using generateTestSemanticsExpressionForCurrentSemanticsTree
|
||||
// then compares it to the code used in the 'generated code is correct' test
|
||||
// below. When you update the implementation of generateTestSemanticsExpressionForCurrentSemanticsTree
|
||||
// also update this code to reflect the new output.
|
||||
//
|
||||
// This test is flexible w.r.t. leading and trailing whitespace.
|
||||
testWidgets('generates code', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
await pumpTestWidget(tester);
|
||||
final String code = semantics
|
||||
.generateTestSemanticsExpressionForCurrentSemanticsTree()
|
||||
.split('\n')
|
||||
.map((String line) => line.trim())
|
||||
.join('\n')
|
||||
.trim() + ',';
|
||||
|
||||
File findThisTestFile(Directory directory) {
|
||||
for (FileSystemEntity entity in directory.listSync()) {
|
||||
if (entity is Directory) {
|
||||
final File childSearch = findThisTestFile(entity);
|
||||
if (childSearch != null) {
|
||||
return childSearch;
|
||||
}
|
||||
} else if (entity is File && entity.path.endsWith('semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart')) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final File thisTestFile = findThisTestFile(Directory.current);
|
||||
expect(thisTestFile, isNotNull);
|
||||
String expectedCode = thisTestFile.readAsStringSync();
|
||||
expectedCode = expectedCode.substring(
|
||||
expectedCode.indexOf('>' * 12) + 12,
|
||||
expectedCode.indexOf('<' * 12) - 3,
|
||||
)
|
||||
.split('\n')
|
||||
.map((String line) => line.trim())
|
||||
.join('\n')
|
||||
.trim();
|
||||
semantics.dispose();
|
||||
expect(code, expectedCode);
|
||||
});
|
||||
|
||||
testWidgets('generated code is correct', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
await pumpTestWidget(tester);
|
||||
expect(
|
||||
semantics,
|
||||
hasSemantics(
|
||||
// The code below delimited by > and < characters is generated by
|
||||
// generateTestSemanticsExpressionForCurrentSemanticsTree function.
|
||||
// You must update it when changing the output generated by
|
||||
// generateTestSemanticsExpressionForCurrentSemanticsTree. Otherwise,
|
||||
// the test 'generates code', defined above, will fail.
|
||||
// >>>>>>>>>>>>
|
||||
new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
new TestSemantics(
|
||||
label: r'Plain text',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
new TestSemantics(
|
||||
flags: <SemanticsFlags>[SemanticsFlags.hasCheckedState, SemanticsFlags.isChecked, SemanticsFlags.isSelected],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.decrease],
|
||||
label: r'Interactive text',
|
||||
value: r'test-value',
|
||||
increasedValue: r'test-increasedValue',
|
||||
decreasedValue: r'test-decreasedValue',
|
||||
hint: r'test-hint',
|
||||
textDirection: TextDirection.rtl,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// <<<<<<<<<<<<
|
||||
ignoreRect: true,
|
||||
ignoreTransform: true,
|
||||
ignoreId: true,
|
||||
)
|
||||
);
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
@ -76,14 +76,18 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
if (intl.DateFormat.localeExists(_localeName)) {
|
||||
_fullYearFormat = new intl.DateFormat.y(_localeName);
|
||||
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName);
|
||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName);
|
||||
_yearMonthFormat = new intl.DateFormat('yMMMM', _localeName);
|
||||
} else if (intl.DateFormat.localeExists(locale.languageCode)) {
|
||||
_fullYearFormat = new intl.DateFormat.y(locale.languageCode);
|
||||
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode);
|
||||
|
||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
|
||||
_yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
|
||||
} else {
|
||||
_fullYearFormat = new intl.DateFormat.y();
|
||||
_mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
|
||||
_longDateFormat = new intl.DateFormat.yMMMMEEEEd();
|
||||
_yearMonthFormat = new intl.DateFormat('yMMMM');
|
||||
}
|
||||
|
||||
@ -115,6 +119,8 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
|
||||
intl.DateFormat _mediumDateFormat;
|
||||
|
||||
intl.DateFormat _longDateFormat;
|
||||
|
||||
intl.DateFormat _yearMonthFormat;
|
||||
|
||||
static String _computeLocaleName(Locale locale) {
|
||||
@ -169,6 +175,11 @@ class GlobalMaterialLocalizations implements MaterialLocalizations {
|
||||
return _mediumDateFormat.format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatFullDate(DateTime date) {
|
||||
return _longDateFormat.format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatMonthYear(DateTime date) {
|
||||
return _yearMonthFormat.format(date);
|
||||
|
Loading…
x
Reference in New Issue
Block a user