Unverified Commit d76e3abf authored by Kostia Sokolovskyi's avatar Kostia Sokolovskyi Committed by GitHub

Fix memory leaks in DateRangePickerDialog. (#136034)

This PR mainly fixes several memory leaks in the `DateRangePickerDialog`.

### Description
- Fixes https://github.com/flutter/flutter/issues/136033 by:
    1) adding a disposal of several `RestorableValue`;
    2) creating a separate `_DayItem` stateful widget that creates/updates/disposes internal `MaterialStatesController`.
- Marks https://github.com/flutter/flutter/issues/136036.

### Tests
- Updates `test/material/date_picker_theme_test.dart` to use `testWidgetsWithLeakTracking`;
- Updates `test/material/date_range_picker_test.dart` to use `testWidgetsWithLeakTracking`.
parent 670e6ba1
......@@ -1353,6 +1353,15 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
registerForRestoration(_autoValidate, 'autovalidate');
}
@override
void dispose() {
_entryMode.dispose();
_selectedStart.dispose();
_selectedEnd.dispose();
_autoValidate.dispose();
super.dispose();
}
void _handleOk() {
if (_entryMode.value == DatePickerEntryMode.input || _entryMode.value == DatePickerEntryMode.inputOnly) {
final _InputDateRangePickerState picker = _inputPickerKey.currentState!;
......@@ -2368,143 +2377,32 @@ class _MonthItemState extends State<_MonthItem> {
}
Widget _buildDayItem(BuildContext context, DateTime dayToBuild, int firstDayOffset, int daysInMonth) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final TextTheme textTheme = theme.textTheme;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
final TextDirection textDirection = Directionality.of(context);
final Color highlightColor = _highlightColor(context);
final int day = dayToBuild.day;
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) || dayToBuild.isBefore(widget.firstDate);
BoxDecoration? decoration;
TextStyle? itemStyle = textTheme.bodyMedium;
final bool isRangeSelected = widget.selectedDateStart != null && widget.selectedDateEnd != null;
final bool isSelectedDayStart = widget.selectedDateStart != null && dayToBuild.isAtSameMomentAs(widget.selectedDateStart!);
final bool isSelectedDayEnd = widget.selectedDateEnd != null && dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!);
final bool isInRange = isRangeSelected &&
dayToBuild.isAfter(widget.selectedDateStart!) &&
dayToBuild.isBefore(widget.selectedDateEnd!);
T? effectiveValue<T>(T? Function(DatePickerThemeData? theme) getProperty) {
return getProperty(datePickerTheme) ?? getProperty(defaults);
}
T? resolve<T>(MaterialStateProperty<T>? Function(DatePickerThemeData? theme) getProperty, Set<MaterialState> states) {
return effectiveValue(
(DatePickerThemeData? theme) {
return getProperty(theme)?.resolve(states);
},
);
}
final Set<MaterialState> states = <MaterialState>{
if (isDisabled) MaterialState.disabled,
if (isSelectedDayStart || isSelectedDayEnd) MaterialState.selected,
};
final Color? dayForegroundColor = resolve<Color?>((DatePickerThemeData? theme) => theme?.dayForegroundColor, states);
final Color? dayBackgroundColor = resolve<Color?>((DatePickerThemeData? theme) => theme?.dayBackgroundColor, states);
final MaterialStateProperty<Color?> dayOverlayColor = MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) => effectiveValue(
(DatePickerThemeData? theme) =>
isInRange
? theme?.rangeSelectionOverlayColor?.resolve(states)
: theme?.dayOverlayColor?.resolve(states),
)
);
_HighlightPainter? highlightPainter;
if (isSelectedDayStart || isSelectedDayEnd) {
// The selected start and end dates gets a circle background
// highlight, and a contrasting text color.
itemStyle = textTheme.bodyMedium?.apply(color: dayForegroundColor);
decoration = BoxDecoration(
color: dayBackgroundColor,
shape: BoxShape.circle,
);
if (isRangeSelected && widget.selectedDateStart != widget.selectedDateEnd) {
final _HighlightPainterStyle style = isSelectedDayStart
? _HighlightPainterStyle.highlightTrailing
: _HighlightPainterStyle.highlightLeading;
highlightPainter = _HighlightPainter(
color: highlightColor,
style: style,
textDirection: textDirection,
);
}
} else if (isInRange) {
// The days within the range get a light background highlight.
highlightPainter = _HighlightPainter(
color: highlightColor,
style: _HighlightPainterStyle.highlightAll,
textDirection: textDirection,
);
} else if (isDisabled) {
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onSurface.withOpacity(0.38));
} else if (DateUtils.isSameDay(widget.currentDate, dayToBuild)) {
// The current day gets a different text color and a circle stroke
// border.
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.primary);
decoration = BoxDecoration(
border: Border.all(color: colorScheme.primary),
shape: BoxShape.circle,
);
}
// 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.
final String semanticLabelSuffix = DateUtils.isSameDay(widget.currentDate, dayToBuild) ? ', ${localizations.currentDateLabel}' : '';
String semanticLabel = '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}$semanticLabelSuffix';
if (isSelectedDayStart) {
semanticLabel = localizations.dateRangeStartDateSemanticLabel(semanticLabel);
} else if (isSelectedDayEnd) {
semanticLabel = localizations.dateRangeEndDateSemanticLabel(semanticLabel);
}
Widget dayWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
label: semanticLabel,
selected: isSelectedDayStart || isSelectedDayEnd,
child: ExcludeSemantics(
child: Text(localizations.formatDecimal(day), style: itemStyle),
),
),
),
final bool isOneDayRange = isRangeSelected && widget.selectedDateStart == widget.selectedDateEnd;
final bool isToday = DateUtils.isSameDay(widget.currentDate, dayToBuild);
return _DayItem(
day: dayToBuild,
focusNode: _dayFocusNodes[day - 1],
onChanged: widget.onChanged,
onFocusChange: _dayFocusChanged,
highlightColor: _highlightColor(context),
isDisabled: isDisabled,
isRangeSelected: isRangeSelected,
isSelectedDayStart: isSelectedDayStart,
isSelectedDayEnd: isSelectedDayEnd,
isInRange: isInRange,
isOneDayRange: isOneDayRange,
isToday: isToday,
);
if (highlightPainter != null) {
dayWidget = CustomPaint(
painter: highlightPainter,
child: dayWidget,
);
}
if (!isDisabled) {
dayWidget = InkResponse(
focusNode: _dayFocusNodes[day - 1],
onTap: () => widget.onChanged(dayToBuild),
radius: _monthItemRowHeight / 2 + 4,
statesController: MaterialStatesController(states),
overlayColor: dayOverlayColor,
onFocusChange: _dayFocusChanged,
child: dayWidget,
);
}
return dayWidget;
}
Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) {
......@@ -2618,6 +2516,194 @@ class _MonthItemState extends State<_MonthItem> {
}
}
class _DayItem extends StatefulWidget {
const _DayItem({
required this.day,
required this.focusNode,
required this.onChanged,
required this.onFocusChange,
required this.highlightColor,
required this.isDisabled,
required this.isRangeSelected,
required this.isSelectedDayStart,
required this.isSelectedDayEnd,
required this.isInRange,
required this.isOneDayRange,
required this.isToday,
});
final DateTime day;
final FocusNode focusNode;
final ValueChanged<DateTime> onChanged;
final ValueChanged<bool> onFocusChange;
final Color highlightColor;
final bool isDisabled;
final bool isRangeSelected;
final bool isSelectedDayStart;
final bool isSelectedDayEnd;
final bool isInRange;
final bool isOneDayRange;
final bool isToday;
@override
State<_DayItem> createState() => _DayItemState();
}
class _DayItemState extends State<_DayItem> {
final MaterialStatesController _statesController = MaterialStatesController();
@override
void dispose() {
_statesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final TextTheme textTheme = theme.textTheme;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context);
final DatePickerThemeData defaults = DatePickerTheme.defaults(context);
final TextDirection textDirection = Directionality.of(context);
final Color highlightColor = widget.highlightColor;
BoxDecoration? decoration;
TextStyle? itemStyle = textTheme.bodyMedium;
T? effectiveValue<T>(T? Function(DatePickerThemeData? theme) getProperty) {
return getProperty(datePickerTheme) ?? getProperty(defaults);
}
T? resolve<T>(MaterialStateProperty<T>? Function(DatePickerThemeData? theme) getProperty, Set<MaterialState> states) {
return effectiveValue(
(DatePickerThemeData? theme) {
return getProperty(theme)?.resolve(states);
},
);
}
final Set<MaterialState> states = <MaterialState>{
if (widget.isDisabled) MaterialState.disabled,
if (widget.isSelectedDayStart || widget.isSelectedDayEnd) MaterialState.selected,
};
_statesController.value = states;
final Color? dayForegroundColor = resolve<Color?>((DatePickerThemeData? theme) => theme?.dayForegroundColor, states);
final Color? dayBackgroundColor = resolve<Color?>((DatePickerThemeData? theme) => theme?.dayBackgroundColor, states);
final MaterialStateProperty<Color?> dayOverlayColor = MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) => effectiveValue(
(DatePickerThemeData? theme) => widget.isInRange
? theme?.rangeSelectionOverlayColor?.resolve(states)
: theme?.dayOverlayColor?.resolve(states),
)
);
_HighlightPainter? highlightPainter;
if (widget.isSelectedDayStart || widget.isSelectedDayEnd) {
// The selected start and end dates gets a circle background
// highlight, and a contrasting text color.
itemStyle = textTheme.bodyMedium?.apply(color: dayForegroundColor);
decoration = BoxDecoration(
color: dayBackgroundColor,
shape: BoxShape.circle,
);
if (widget.isRangeSelected && !widget.isOneDayRange) {
final _HighlightPainterStyle style = widget.isSelectedDayStart
? _HighlightPainterStyle.highlightTrailing
: _HighlightPainterStyle.highlightLeading;
highlightPainter = _HighlightPainter(
color: highlightColor,
style: style,
textDirection: textDirection,
);
}
} else if (widget.isInRange) {
// The days within the range get a light background highlight.
highlightPainter = _HighlightPainter(
color: highlightColor,
style: _HighlightPainterStyle.highlightAll,
textDirection: textDirection,
);
} else if (widget.isDisabled) {
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onSurface.withOpacity(0.38));
} else if (widget.isToday) {
// The current day gets a different text color and a circle stroke
// border.
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.primary);
decoration = BoxDecoration(
border: Border.all(color: colorScheme.primary),
shape: BoxShape.circle,
);
}
final String dayText = localizations.formatDecimal(widget.day.day);
// 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.
final String semanticLabelSuffix = widget.isToday ? ', ${localizations.currentDateLabel}' : '';
String semanticLabel = '$dayText, ${localizations.formatFullDate(widget.day)}$semanticLabelSuffix';
if (widget.isSelectedDayStart) {
semanticLabel = localizations.dateRangeStartDateSemanticLabel(semanticLabel);
} else if (widget.isSelectedDayEnd) {
semanticLabel = localizations.dateRangeEndDateSemanticLabel(semanticLabel);
}
Widget dayWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
label: semanticLabel,
selected: widget.isSelectedDayStart || widget.isSelectedDayEnd,
child: ExcludeSemantics(
child: Text(dayText, style: itemStyle),
),
),
),
);
if (highlightPainter != null) {
dayWidget = CustomPaint(
painter: highlightPainter,
child: dayWidget,
);
}
if (!widget.isDisabled) {
dayWidget = InkResponse(
focusNode: widget.focusNode,
onTap: () => widget.onChanged(widget.day),
radius: _monthItemRowHeight / 2 + 4,
statesController: _statesController,
overlayColor: dayOverlayColor,
onFocusChange: widget.onFocusChange,
child: dayWidget,
);
}
return dayWidget;
}
}
/// Determines which style to use to paint the highlight.
enum _HighlightPainterStyle {
/// Paints nothing.
......
......@@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
void main() {
const DatePickerThemeData datePickerTheme = DatePickerThemeData(
......@@ -136,7 +137,7 @@ void main() {
expect(theme.confirmButtonStyle, null);
});
testWidgets('DatePickerTheme.defaults M3 defaults', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerTheme.defaults M3 defaults', (WidgetTester tester) async {
late final DatePickerThemeData m3; // M3 Defaults
late final ThemeData theme;
late final ColorScheme colorScheme;
......@@ -213,7 +214,7 @@ void main() {
expect(m3.confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString()));
});
testWidgets('DatePickerTheme.defaults M2 defaults', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerTheme.defaults M2 defaults', (WidgetTester tester) async {
late final DatePickerThemeData m2; // M2 defaults
late final ThemeData theme;
late final ColorScheme colorScheme;
......@@ -282,7 +283,7 @@ void main() {
expect(m2.confirmButtonStyle.toString(), equalsIgnoringHashCodes(TextButton.styleFrom().toString()));
});
testWidgets('Default DatePickerThemeData debugFillProperties', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default DatePickerThemeData debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const DatePickerThemeData().debugFillProperties(builder);
......@@ -294,7 +295,7 @@ void main() {
expect(description, <String>[]);
});
testWidgets('DatePickerThemeData implements debugFillProperties', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerThemeData implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
datePickerTheme.debugFillProperties(builder);
......@@ -344,7 +345,7 @@ void main() {
]));
});
testWidgets('DatePickerDialog uses ThemeData datePicker theme (calendar mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerDialog uses ThemeData datePicker theme (calendar mode)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
......@@ -445,7 +446,7 @@ void main() {
expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(datePickerTheme.confirmButtonStyle.toString()));
});
testWidgets('DatePickerDialog uses ThemeData datePicker theme (input mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerDialog uses ThemeData datePicker theme (input mode)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
......@@ -492,7 +493,7 @@ void main() {
expect(confirmButtonStyle.toString(), equalsIgnoringHashCodes(datePickerTheme.confirmButtonStyle.toString()));
});
testWidgets('DateRangePickerDialog uses ThemeData datePicker theme', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DateRangePickerDialog uses ThemeData datePicker theme', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
......@@ -551,9 +552,14 @@ void main() {
await gesture.moveTo(tester.getCenter(find.text('18')));
await tester.pumpAndSettle();
expect(inkFeatures, paints..circle(color: datePickerTheme.rangeSelectionOverlayColor?.resolve(<MaterialState>{})));
});
testWidgets('Dividers use DatePickerThemeData.dividerColor', (WidgetTester tester) async {
},
leakTrackingTestConfig: const LeakTrackingTestConfig(
// TODO(ksokolovskyi): remove after fixing
// https://github.com/flutter/flutter/issues/136036
notDisposedAllowList: <String, int?> {'AnnotatedRegionLayer<SystemUiOverlayStyle>': 2},
));
testWidgetsWithLeakTracking('Dividers use DatePickerThemeData.dividerColor', (WidgetTester tester) async {
Future<void> showPicker(WidgetTester tester, Size size) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1.0;
......@@ -594,7 +600,7 @@ void main() {
expect(horizontalDivider.color, datePickerTheme.dividerColor);
});
testWidgets(
testWidgetsWithLeakTracking(
'DatePicker uses ThemeData.inputDecorationTheme properties '
'which are null in DatePickerThemeData.inputDecorationTheme',
(WidgetTester tester) async {
......@@ -650,7 +656,7 @@ void main() {
expect(inputDecoration.border , const OutlineInputBorder());
});
testWidgets('DatePickerDialog resolves DatePickerTheme.dayOverlayColor states', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerDialog resolves DatePickerTheme.dayOverlayColor states', (WidgetTester tester) async {
final MaterialStateProperty<Color> dayOverlayColor = MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return const Color(0xff00ff00);
......@@ -742,7 +748,7 @@ void main() {
);
});
testWidgets('DatePickerDialog resolves DatePickerTheme.yearOverlayColor states', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerDialog resolves DatePickerTheme.yearOverlayColor states', (WidgetTester tester) async {
final MaterialStateProperty<Color> yearOverlayColor = MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return const Color(0xff00ff00);
......@@ -824,7 +830,7 @@ void main() {
);
});
testWidgets('DateRangePickerDialog resolves DatePickerTheme.rangeSelectionOverlayColor states', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DateRangePickerDialog resolves DatePickerTheme.rangeSelectionOverlayColor states', (WidgetTester tester) async {
final MaterialStateProperty<Color> rangeSelectionOverlayColor = MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return const Color(0xff00ff00);
......@@ -896,5 +902,10 @@ void main() {
..circle(color: rangeSelectionOverlayColor.resolve(<MaterialState>{MaterialState.pressed})),
);
}
});
},
leakTrackingTestConfig: const LeakTrackingTestConfig(
// TODO(ksokolovskyi): remove after fixing
// https://github.com/flutter/flutter/issues/136036
notDisposedAllowList: <String, int?> {'AnnotatedRegionLayer<SystemUiOverlayStyle>': 2},
));
}
......@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'feedback_tester.dart';
void main() {
......@@ -111,7 +112,7 @@ void main() {
await callback(range);
}
testWidgets('Default layout (calendar mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default layout (calendar mode)', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Finder helpText = find.text('Select range');
final Finder firstDateHeaderText = find.text('Jan 15');
......@@ -173,7 +174,7 @@ void main() {
}, useMaterial3: true);
});
testWidgets('Default Dialog properties (calendar mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default Dialog properties (calendar mode)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Material dialogMaterial = tester.widget<Material>(
......@@ -193,7 +194,7 @@ void main() {
}, useMaterial3: theme.useMaterial3);
});
testWidgets('Default Dialog properties (input mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default Dialog properties (input mode)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Material dialogMaterial = tester.widget<Material>(
......@@ -213,7 +214,7 @@ void main() {
}, useMaterial3: theme.useMaterial3);
});
testWidgets('Scaffold and AppBar defaults', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Scaffold and AppBar defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Scaffold scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
......@@ -244,14 +245,14 @@ void main() {
await preparePicker(tester, (Future<DateTimeRange?> range) async { }, useMaterial3: true);
}
testWidgets('portrait', (WidgetTester tester) async {
testWidgetsWithLeakTracking('portrait', (WidgetTester tester) async {
await showPicker(tester, kCommonScreenSizePortrait);
expect(tester.widget<Text>(find.text('Jan 15 – Jan 25, 2016')).style?.fontSize, 32);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
});
testWidgets('landscape', (WidgetTester tester) async {
testWidgetsWithLeakTracking('landscape', (WidgetTester tester) async {
await showPicker(tester, kCommonScreenSizeLandscape);
expect(tester.widget<Text>(find.text('Jan 15 – Jan 25, 2016')).style?.fontSize, 24);
await tester.tap(find.text('Cancel'));
......@@ -259,7 +260,7 @@ void main() {
});
});
testWidgets('Save and help text is used', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Save and help text is used', (WidgetTester tester) async {
helpText = 'help';
saveText = 'make it so';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -268,14 +269,14 @@ void main() {
});
});
testWidgets('Material3 has sentence case labels', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Material3 has sentence case labels', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.text('Save'), findsOneWidget);
expect(find.text('Select range'), findsOneWidget);
}, useMaterial3: true);
});
testWidgets('Initial date is the default', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Initial date is the default', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('SAVE'));
expect(
......@@ -288,7 +289,7 @@ void main() {
});
});
testWidgets('Last month header should be visible if last date is selected', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Last month header should be visible if last date is selected', (WidgetTester tester) async {
firstDate = DateTime(2015);
lastDate = DateTime(2016, DateTime.december, 31);
initialDateRange = DateTimeRange(
......@@ -302,7 +303,7 @@ void main() {
});
});
testWidgets('First month header should be visible if first date is selected', (WidgetTester tester) async {
testWidgetsWithLeakTracking('First month header should be visible if first date is selected', (WidgetTester tester) async {
firstDate = DateTime(2015);
lastDate = DateTime(2016, DateTime.december, 31);
initialDateRange = DateTimeRange(
......@@ -317,7 +318,7 @@ void main() {
});
});
testWidgets('Current month header should be visible if no date is selected', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Current month header should be visible if no date is selected', (WidgetTester tester) async {
firstDate = DateTime(2015);
lastDate = DateTime(2016, DateTime.december, 31);
currentDate = DateTime(2016, DateTime.september);
......@@ -331,14 +332,14 @@ void main() {
});
});
testWidgets('Can cancel', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can cancel', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.byIcon(Icons.close));
expect(await range, isNull);
});
});
testWidgets('Can select a range', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can select a range', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.tap(find.text('14').first);
......@@ -350,7 +351,7 @@ void main() {
});
});
testWidgets('Tapping earlier date resets selected range', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Tapping earlier date resets selected range', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.tap(find.text('11').first);
......@@ -363,7 +364,7 @@ void main() {
});
});
testWidgets('Can select single day range', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can select single day range', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.tap(find.text('12').first);
......@@ -375,7 +376,7 @@ void main() {
});
});
testWidgets('Cannot select a day outside bounds', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Cannot select a day outside bounds', (WidgetTester tester) async {
initialDateRange = DateTimeRange(
start: DateTime(2017, DateTime.january, 13),
end: DateTime(2017, DateTime.january, 15),
......@@ -393,7 +394,7 @@ void main() {
});
});
testWidgets('Can switch from calendar to input entry mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can switch from calendar to input entry mode', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNothing);
await tester.tap(find.byIcon(Icons.edit));
......@@ -402,7 +403,7 @@ void main() {
});
});
testWidgets('Can switch from input to calendar entry mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can switch from input to calendar entry mode', (WidgetTester tester) async {
initialEntryMode = DatePickerEntryMode.input;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNWidgets(2));
......@@ -412,7 +413,7 @@ void main() {
});
});
testWidgets('Can not switch out of calendarOnly mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can not switch out of calendarOnly mode', (WidgetTester tester) async {
initialEntryMode = DatePickerEntryMode.calendarOnly;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNothing);
......@@ -420,7 +421,7 @@ void main() {
});
});
testWidgets('Can not switch out of inputOnly mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can not switch out of inputOnly mode', (WidgetTester tester) async {
initialEntryMode = DatePickerEntryMode.inputOnly;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNWidgets(2));
......@@ -428,7 +429,7 @@ void main() {
});
});
testWidgets('Input only mode should validate date', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Input only mode should validate date', (WidgetTester tester) async {
initialEntryMode = DatePickerEntryMode.inputOnly;
errorInvalidText = 'oops';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -442,7 +443,7 @@ void main() {
});
});
testWidgets('Switching to input mode keeps selected date', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Switching to input mode keeps selected date', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.tap(find.text('14').first);
......@@ -461,7 +462,7 @@ void main() {
initialEntryMode = DatePickerEntryMode.input;
});
testWidgets('Invalid start date', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Invalid start date', (WidgetTester tester) async {
// Invalid start date should have neither a start nor end date selected in
// calendar mode
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -475,7 +476,7 @@ void main() {
});
});
testWidgets('Invalid end date', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Invalid end date', (WidgetTester tester) async {
// Invalid end date should only have a start date selected
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/24/2016');
......@@ -488,7 +489,7 @@ void main() {
});
});
testWidgets('Invalid range', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Invalid range', (WidgetTester tester) async {
// Start date after end date should just use the start date
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/25/2016');
......@@ -502,7 +503,7 @@ void main() {
});
});
testWidgets('OK Cancel button layout', (WidgetTester tester) async {
testWidgetsWithLeakTracking('OK Cancel button layout', (WidgetTester tester) async {
Widget buildFrame(TextDirection textDirection) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
......@@ -577,7 +578,7 @@ void main() {
feedback.dispose();
});
testWidgets('Selecting dates vibrates', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Selecting dates vibrates', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('10').first);
await tester.pump(hapticFeedbackInterval);
......@@ -591,7 +592,7 @@ void main() {
});
});
testWidgets('Tapping unselectable date does not vibrate', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Tapping unselectable date does not vibrate', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('8').first);
await tester.pump(hapticFeedbackInterval);
......@@ -601,7 +602,7 @@ void main() {
});
group('Keyboard navigation', () {
testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can toggle to calendar entry mode', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNothing);
// Navigate to the entry toggle button and activate it
......@@ -614,7 +615,7 @@ void main() {
});
});
testWidgets('Can navigate date grid with arrow keys', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can navigate date grid with arrow keys', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
// Navigate to the grid
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
......@@ -666,7 +667,7 @@ void main() {
});
});
testWidgets('Navigating with arrow keys scrolls as needed', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Navigating with arrow keys scrolls as needed', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
// Jan and Feb headers should be showing, but no March
expect(find.text('January 2016'), findsOneWidget);
......@@ -731,7 +732,7 @@ void main() {
});
});
testWidgets('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async {
testWidgetsWithLeakTracking('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
// Navigate to the grid
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
......@@ -790,7 +791,7 @@ void main() {
initialEntryMode = DatePickerEntryMode.input;
});
testWidgets('Default Dialog properties (input mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default Dialog properties (input mode)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Material dialogMaterial = tester.widget<Material>(
......@@ -813,7 +814,7 @@ void main() {
}, useMaterial3: theme.useMaterial3);
});
testWidgets('Default InputDecoration', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default InputDecoration', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final InputDecoration startDateDecoration = tester.widget<TextField>(
find.byType(TextField).first).decoration!;
......@@ -833,13 +834,13 @@ void main() {
}, useMaterial3: true);
});
testWidgets('Initial entry mode is used', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Initial entry mode is used', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNWidgets(2));
});
});
testWidgets('All custom strings are used', (WidgetTester tester) async {
testWidgetsWithLeakTracking('All custom strings are used', (WidgetTester tester) async {
initialDateRange = null;
cancelText = 'nope';
confirmText = 'yep';
......@@ -859,7 +860,7 @@ void main() {
});
});
testWidgets('Initial date is the default', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Initial date is the default', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('OK'));
expect(await range, DateTimeRange(
......@@ -869,7 +870,7 @@ void main() {
});
});
testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Can toggle to calendar entry mode', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNWidgets(2));
await tester.tap(find.byIcon(Icons.calendar_today));
......@@ -878,7 +879,7 @@ void main() {
});
});
testWidgets('Toggle to calendar mode keeps selected date', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Toggle to calendar mode keeps selected date', (WidgetTester tester) async {
initialDateRange = null;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/25/2016');
......@@ -894,7 +895,7 @@ void main() {
});
});
testWidgets('Entered text returns range', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Entered text returns range', (WidgetTester tester) async {
initialDateRange = null;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/25/2016');
......@@ -908,7 +909,7 @@ void main() {
});
});
testWidgets('Too short entered text shows error', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Too short entered text shows error', (WidgetTester tester) async {
initialDateRange = null;
errorFormatText = 'oops';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -922,7 +923,7 @@ void main() {
});
});
testWidgets('Bad format entered text shows error', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Bad format entered text shows error', (WidgetTester tester) async {
initialDateRange = null;
errorFormatText = 'oops';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -936,7 +937,7 @@ void main() {
});
});
testWidgets('Invalid entered text shows error', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Invalid entered text shows error', (WidgetTester tester) async {
initialDateRange = null;
errorInvalidText = 'oops';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -950,7 +951,7 @@ void main() {
});
});
testWidgets('End before start date shows error', (WidgetTester tester) async {
testWidgetsWithLeakTracking('End before start date shows error', (WidgetTester tester) async {
initialDateRange = null;
errorInvalidRangeText = 'oops';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -964,7 +965,7 @@ void main() {
});
});
testWidgets('Error text only displayed for invalid date', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Error text only displayed for invalid date', (WidgetTester tester) async {
initialDateRange = null;
errorInvalidText = 'oops';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -978,7 +979,7 @@ void main() {
});
});
testWidgets('End before start date does not get passed to calendar mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('End before start date does not get passed to calendar mode', (WidgetTester tester) async {
initialDateRange = null;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.enterText(find.byType(TextField).at(0), '12/27/2016');
......@@ -996,7 +997,7 @@ void main() {
});
});
testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async {
testWidgetsWithLeakTracking('InputDecorationTheme is honored', (WidgetTester tester) async {
// Given a custom paint for an input decoration, extract the border and
// fill color and test them against the expected values.
......@@ -1062,7 +1063,7 @@ void main() {
});
// This is a regression test for https://github.com/flutter/flutter/issues/131989.
testWidgets('Dialog contents do not overflow when resized from landscape to portrait',
testWidgetsWithLeakTracking('Dialog contents do not overflow when resized from landscape to portrait',
(WidgetTester tester) async {
addTearDown(tester.view.reset);
// Initial window size is wide for landscape mode.
......@@ -1078,7 +1079,7 @@ void main() {
});
});
testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DatePickerDialog is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
......@@ -1134,7 +1135,7 @@ void main() {
expect(find.text('12/1/2021 to 14/1/2021'), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('DateRangePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DateRangePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
restorationScopeId: 'app',
......@@ -1184,7 +1185,7 @@ void main() {
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
group('showDateRangePicker avoids overlapping display features', () {
testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
testWidgetsWithLeakTracking('positioning with anchorPoint', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
......@@ -1221,7 +1222,7 @@ void main() {
expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(800.0, 600.0));
});
testWidgets('positioning with Directionality', (WidgetTester tester) async {
testWidgetsWithLeakTracking('positioning with Directionality', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
......@@ -1261,7 +1262,7 @@ void main() {
expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(800.0, 600.0));
});
testWidgets('positioning with defaults', (WidgetTester tester) async {
testWidgetsWithLeakTracking('positioning with defaults', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
builder: (BuildContext context, Widget? child) {
......@@ -1299,7 +1300,7 @@ void main() {
});
group('Semantics', () {
testWidgets('calendar mode', (WidgetTester tester) async {
testWidgetsWithLeakTracking('calendar mode', (WidgetTester tester) async {
final SemanticsHandle semantics = tester.ensureSemantics();
currentDate = DateTime(2016, DateTime.january, 30);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
......@@ -1317,7 +1318,7 @@ void main() {
});
for (final TextInputType? keyboardType in <TextInputType?>[null, TextInputType.emailAddress]) {
testWidgets('DateRangePicker takes keyboardType $keyboardType', (WidgetTester tester) async {
testWidgetsWithLeakTracking('DateRangePicker takes keyboardType $keyboardType', (WidgetTester tester) async {
late BuildContext buttonContext;
const InputBorder border = InputBorder.none;
await tester.pumpWidget(MaterialApp(
......@@ -1370,7 +1371,7 @@ void main() {
});
}
testWidgets('honors switchToInputEntryModeIcon', (WidgetTester tester) async {
testWidgetsWithLeakTracking('honors switchToInputEntryModeIcon', (WidgetTester tester) async {
Widget buildApp({bool? useMaterial3, Icon? switchToInputEntryModeIcon}) {
return MaterialApp(
theme: ThemeData(
......@@ -1425,7 +1426,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('honors switchToCalendarEntryModeIcon', (WidgetTester tester) async {
testWidgetsWithLeakTracking('honors switchToCalendarEntryModeIcon', (WidgetTester tester) async {
Widget buildApp({bool? useMaterial3, Icon? switchToCalendarEntryModeIcon}) {
return MaterialApp(
theme: ThemeData(
......@@ -1488,7 +1489,7 @@ void main() {
// support is deprecated and the APIs are removed, these tests
// can be deleted.
testWidgets('Default layout (calendar mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default layout (calendar mode)', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Finder helpText = find.text('SELECT RANGE');
final Finder firstDateHeaderText = find.text('Jan 15');
......@@ -1544,7 +1545,7 @@ void main() {
});
});
testWidgets('Default Dialog properties (calendar mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default Dialog properties (calendar mode)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Material dialogMaterial = tester.widget<Material>(
......@@ -1564,7 +1565,7 @@ void main() {
});
});
testWidgets('Scaffold and AppBar defaults', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Scaffold and AppBar defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Scaffold scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
......@@ -1591,7 +1592,7 @@ void main() {
initialEntryMode = DatePickerEntryMode.input;
});
testWidgets('Default Dialog properties (input mode)', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default Dialog properties (input mode)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final Material dialogMaterial = tester.widget<Material>(
......@@ -1614,7 +1615,7 @@ void main() {
});
});
testWidgets('Default InputDecoration', (WidgetTester tester) async {
testWidgetsWithLeakTracking('Default InputDecoration', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final InputDecoration startDateDecoration = tester.widget<TextField>(
find.byType(TextField).first).decoration!;
......@@ -1666,6 +1667,14 @@ class _RestorableDateRangePickerDialogTestWidgetState extends State<_RestorableD
},
);
@override
void dispose() {
_startDate.dispose();
_endDate.dispose();
_restorableDateRangePickerRouteFuture.dispose();
super.dispose();
}
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_startDate, 'start_date');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment