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