Unverified Commit 414bff14 authored by Kostia Sokolovskyi's avatar Kostia Sokolovskyi Committed by GitHub

ScrollController creation dispatching for memory leaks tracking (#133759)

parent 7fe4571a
......@@ -884,6 +884,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
@override
void dispose() {
focusScopeNode.dispose();
primaryScrollController.dispose();
super.dispose();
}
......
......@@ -65,7 +65,11 @@ class ScrollController extends ChangeNotifier {
this.debugLabel,
this.onAttach,
this.onDetach,
}) : _initialScrollOffset = initialScrollOffset;
}) : _initialScrollOffset = initialScrollOffset {
if (kFlutterMemoryAllocationsEnabled) {
maybeDispatchObjectCreation();
}
}
/// The initial value to use for [offset].
///
......
......@@ -1609,15 +1609,19 @@ void main() {
// Generates a MaterialApp with a SliverAppBar in a CustomScrollView.
// The first cell of the scroll view contains a button at its top, and is
// initially scrolled so that it is beneath the SliverAppBar.
Widget buildWidget({
(ScrollController, Widget) buildWidget({
required bool forceMaterialTransparency,
required VoidCallback onPressed
}) {
const double appBarHeight = 120;
return MaterialApp(
final ScrollController controller = ScrollController(initialScrollOffset: appBarHeight);
return (
controller,
MaterialApp(
home: Scaffold(
body: CustomScrollView(
controller: ScrollController(initialScrollOffset:appBarHeight),
controller: controller,
slivers: <Widget>[
SliverAppBar(
collapsedHeight: appBarHeight,
......@@ -1644,13 +1648,14 @@ void main() {
),
]),
),
),
);
}
testWidgetsWithLeakTracking(
'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
bool buttonWasPressed = false;
final Widget widget = buildWidget(
final (ScrollController controller, Widget widget) = buildWidget(
forceMaterialTransparency:true,
onPressed:() { buttonWasPressed = true; },
);
......@@ -1663,6 +1668,8 @@ void main() {
await tester.tap(buttonFinder);
await tester.pump();
expect(buttonWasPressed, isTrue);
controller.dispose();
});
testWidgetsWithLeakTracking(
......@@ -1672,7 +1679,7 @@ void main() {
WidgetController.hitTestWarningShouldBeFatal = false;
bool buttonWasPressed = false;
final Widget widget = buildWidget(
final (ScrollController controller, Widget widget) = buildWidget(
forceMaterialTransparency:false,
onPressed:() { buttonWasPressed = true; },
);
......@@ -1685,6 +1692,8 @@ void main() {
await tester.tap(buttonFinder, warnIfMissed:false);
await tester.pump();
expect(buttonWasPressed, isFalse);
controller.dispose();
});
});
......@@ -4295,6 +4304,8 @@ void main() {
);
expect(tester.takeException(), isNull);
controller.dispose();
});
testWidgetsWithLeakTracking('does not trigger on horizontal scroll', (WidgetTester tester) async {
......
......@@ -84,7 +84,14 @@ void main() {
await tester.pump();
expect(tabController.index, 2);
expect(indicatorColors(tester), const <Color>[kUnselectedColor, kUnselectedColor, kSelectedColor]);
});
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 1,
},
));
testWidgetsWithLeakTracking('PageSelector responds correctly to TabController.animateTo()', (WidgetTester tester) async {
final TabController tabController = TabController(
......@@ -127,7 +134,14 @@ void main() {
await tester.pumpAndSettle();
expect(tabController.index, 2);
expect(indicatorColors(tester), const <Color>[kUnselectedColor, kUnselectedColor, kSelectedColor]);
});
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 1,
},
));
testWidgetsWithLeakTracking('PageSelector responds correctly to TabBarView drags', (WidgetTester tester) async {
final TabController tabController = TabController(
......@@ -185,8 +199,14 @@ void main() {
await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 1000.0);
await tester.pumpAndSettle();
expect(indicatorColors(tester), const <Color>[kUnselectedColor, kSelectedColor, kUnselectedColor]);
});
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 1,
},
));
testWidgetsWithLeakTracking('PageSelector indicatorColors', (WidgetTester tester) async {
const Color kRed = Color(0xFFFF0000);
......@@ -205,7 +225,14 @@ void main() {
tabController.index = 0;
await tester.pumpAndSettle();
expect(indicatorColors(tester), const <Color>[kBlue, kRed, kRed]);
});
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 1,
},
));
testWidgets('PageSelector indicatorSize', (WidgetTester tester) async {
final TabController tabController = TabController(
......@@ -272,5 +299,12 @@ void main() {
for (final TabPageSelectorIndicator indicator in indicators) {
expect(indicator.borderStyle, BorderStyle.solid);
}
});
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 1,
},
));
}
......@@ -423,6 +423,8 @@ void main() {
expect(controller.offset, greaterThan(lastScrollOffset));
expect(controller.offset, lessThan(0.0));
expect(refreshCalled, isTrue);
controller.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgetsWithLeakTracking('RefreshIndicator does not force child to relayout', (WidgetTester tester) async {
......@@ -618,6 +620,8 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0));
scrollController.dispose();
});
testWidgetsWithLeakTracking('Reverse RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
......@@ -654,6 +658,8 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0));
scrollController.dispose();
});
// Regression test for https://github.com/flutter/flutter/issues/71936
......@@ -691,6 +697,8 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(find.byType(RefreshProgressIndicator), findsNothing);
scrollController.dispose();
});
testWidgetsWithLeakTracking('Top RefreshIndicator(onEdge mode) should not be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
......@@ -725,6 +733,8 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(find.byType(RefreshProgressIndicator), findsNothing);
scrollController.dispose();
});
testWidgetsWithLeakTracking('Reverse RefreshIndicator(onEdge mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
......@@ -760,6 +770,8 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(find.byType(RefreshProgressIndicator), findsNothing);
scrollController.dispose();
});
testWidgetsWithLeakTracking('ScrollController.jumpTo should not trigger the refresh indicator', (WidgetTester tester) async {
......@@ -792,6 +804,8 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the indicator hide animation
expect(refreshCalled, false);
scrollController.dispose();
});
testWidgetsWithLeakTracking('RefreshIndicator.adaptive', (WidgetTester tester) async {
......
......@@ -203,6 +203,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
controller.dispose();
});
testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async {
......@@ -229,6 +231,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), paints..rect());
controller.dispose();
});
testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async {
......@@ -261,6 +265,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), paints..rect());
controller.dispose();
});
testWidgetsWithLeakTracking(
......@@ -314,6 +320,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
controller.dispose();
},
);
......@@ -341,6 +349,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), paints..rect());
controller.dispose();
});
testWidgetsWithLeakTracking('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async {
......@@ -373,6 +383,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), paints..rect());
controller.dispose();
});
testWidgetsWithLeakTracking('On first render with thumbVisibility: false, the thumb is hidden', (WidgetTester tester) async {
......@@ -399,6 +411,8 @@ void main() {
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), isNot(paints..rect()));
controller.dispose();
});
testWidgetsWithLeakTracking(
......@@ -452,6 +466,8 @@ void main() {
await tester.pumpAndSettle();
// Scrollbar is not showing after scroll finishes
expect(find.byType(Scrollbar), isNot(paints..rect()));
controller.dispose();
},
);
......@@ -501,6 +517,8 @@ void main() {
await tester.pumpAndSettle();
// Scrollbar is not showing after scroll finishes
expect(find.byType(Scrollbar), paints..rect());
controller.dispose();
},
);
......@@ -561,6 +579,8 @@ void main() {
await tester.pumpAndSettle();
// Scrollbar thumb is showing after scroll finishes and timer ends.
expect(find.byType(Scrollbar), paints..rect());
controller.dispose();
},
);
......@@ -610,6 +630,8 @@ void main() {
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(materialScrollbar, isNot(paints..rect()));
controller.dispose();
},
);
......@@ -673,6 +695,8 @@ void main() {
));
await tester.pumpAndSettle();
controller.dispose();
});
testWidgetsWithLeakTracking('Tapping the track area pages the Scroll View', (WidgetTester tester) async {
......@@ -763,6 +787,8 @@ void main() {
color: _kAndroidThumbIdleColor,
),
);
scrollController.dispose();
});
testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async {
......@@ -936,6 +962,8 @@ void main() {
color: _kAndroidThumbIdleColor,
),
);
scrollController.dispose();
});
testWidgetsWithLeakTracking('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async {
......@@ -1331,6 +1359,8 @@ void main() {
expect(find.byType(CupertinoScrollbar), paints..rrect());
final CupertinoScrollbar scrollbar = tester.widget<CupertinoScrollbar>(find.byType(CupertinoScrollbar));
expect(scrollbar.controller, isNotNull);
controller.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgetsWithLeakTracking("Scrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async {
......@@ -1463,6 +1493,8 @@ void main() {
await tester.pumpAndSettle();
// The offset should not have changed.
expect(scrollController.offset, scrollAmount);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgetsWithLeakTracking('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async {
......@@ -1557,6 +1589,8 @@ void main() {
// The offset should not have changed.
expect(scrollController.offset, scrollAmount * 2);
expect(tapCount, 2);
scrollController.dispose();
});
testWidgetsWithLeakTracking('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
......@@ -1729,6 +1763,8 @@ void main() {
color: const Color(0xffbcbcbc),
),
);
scrollController.dispose();
});
testWidgetsWithLeakTracking('Scrollbar.thumbVisibility triggers assertion when multiple ScrollPositions are attached.', (WidgetTester tester) async {
......@@ -1796,7 +1832,16 @@ void main() {
error.message,
contains('The provided ScrollController is currently attached to more than one ScrollPosition.'),
);
});
scrollController.dispose();
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 2,
},
));
testWidgetsWithLeakTracking('Scrollbar scrollOrientation works correctly', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
......@@ -1844,5 +1889,7 @@ void main() {
color: _kAndroidThumbIdleColor,
),
);
scrollController.dispose();
});
}
......@@ -113,6 +113,8 @@ void main() {
color: const Color(0x80000000),
),
);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
......@@ -204,6 +206,8 @@ void main() {
color: const Color(0xff2196f3),
),
);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
......@@ -253,6 +257,8 @@ void main() {
color: const Color(0xFF000000),
),
);
scrollController.dispose();
},
);
......@@ -301,6 +307,8 @@ void main() {
color: _kDefaultIdleThumbColor,
),
);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgetsWithLeakTracking('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async {
......@@ -349,6 +357,8 @@ void main() {
color: _kDefaultIdleThumbColor,
),
);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgetsWithLeakTracking('Scrollbar widget properties take priority over theme', (WidgetTester tester) async {
......@@ -442,6 +452,8 @@ void main() {
color: const Color(0x80000000),
),
);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
......@@ -451,9 +463,11 @@ void main() {
);
testWidgetsWithLeakTracking('ThemeData colorScheme is used when no ScrollbarTheme is set', (WidgetTester tester) async {
Widget buildFrame(ThemeData appTheme) {
(ScrollController, Widget) buildFrame(ThemeData appTheme) {
final ScrollController scrollController = ScrollController();
return MaterialApp(
return (
scrollController,
MaterialApp(
theme: appTheme,
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
......@@ -467,14 +481,16 @@ void main() {
),
),
),
),
);
}
// Scrollbar defaults for light themes:
// - coloring based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData(
final (ScrollController controller1, Widget frame1) = buildFrame(ThemeData(
colorScheme: const ColorScheme.light(),
)));
));
await tester.pumpWidget(frame1);
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
......@@ -545,9 +561,10 @@ void main() {
// Scrollbar defaults for dark themes:
// - coloring slightly different based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData(
final (ScrollController controller2, Widget frame2) = buildFrame(ThemeData(
colorScheme: const ColorScheme.dark(),
)));
));
await tester.pumpWidget(frame2);
await tester.pumpAndSettle(); // Theme change animation
// Idle scrollbar behavior
......@@ -610,6 +627,9 @@ void main() {
color: const Color(0xa6ffffff),
),
);
controller1.dispose();
controller2.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
......@@ -656,6 +676,8 @@ void main() {
)
..rrect(color: const Color(0xff4caf50)),
);
scrollController.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
......
......@@ -81,5 +81,12 @@ void main() {
// should not crash.
await tester.tap(find.text('Tab 2'));
await tester.pumpAndSettle();
});
},
// TODO(someone): remove after fixing
// https://github.com/flutter/flutter/issues/133755
leakTrackingTestConfig: const LeakTrackingTestConfig(
notDisposedAllowList: <String, int?>{
'PageController': 1,
},
));
}
......@@ -4,6 +4,7 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -393,4 +394,21 @@ void main() {
// should have been true
expect(isScrolling, isTrue);
});
test('$ScrollController dispatches object creation in constructor', () {
final List<ObjectEvent> events = <ObjectEvent>[];
void listener(ObjectEvent event) {
if (event.object.runtimeType == ScrollController) {
events.add(event);
}
}
MemoryAllocations.instance.addListener(listener);
final ScrollController controller = ScrollController();
expect(events, hasLength(1));
controller.dispose();
MemoryAllocations.instance.removeListener(listener);
});
}
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