From 2d9a075dfc780dec8003e96eeea0ae83f27f932a Mon Sep 17 00:00:00 2001 From: Kostia Sokolovskyi Date: Mon, 6 Nov 2023 23:10:50 +0100 Subject: [PATCH] Cover text_selection tests with leak tracking. (#137009) --- .../flutter/lib/src/rendering/editable.dart | 13 +- .../test/widgets/text_selection_test.dart | 183 ++++++++++++------ ...election_toolbar_layout_delegate_test.dart | 3 +- 3 files changed, 141 insertions(+), 58 deletions(-) diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index c7b02913b17..f48a4e5f9ff 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -364,7 +364,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, _readOnly = readOnly, _forceLine = forceLine, _clipBehavior = clipBehavior, - _hasFocus = hasFocus ?? false { + _hasFocus = hasFocus ?? false, + _disposeShowCursor = showCursor == null { assert(!_showCursor.value || cursorColor != null); _selectionPainter.highlightColor = selectionColor; @@ -405,6 +406,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, _selectionPainter.dispose(); _caretPainter.dispose(); _textPainter.dispose(); + if (_disposeShowCursor) { + _showCursor.dispose(); + _disposeShowCursor = false; + } super.dispose(); } @@ -879,6 +884,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, _caretPainter.backgroundCursorColor = value; } + bool _disposeShowCursor; + /// Whether to paint the cursor. ValueNotifier get showCursor => _showCursor; ValueNotifier _showCursor; @@ -889,6 +896,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, if (attached) { _showCursor.removeListener(_showHideCursor); } + if (_disposeShowCursor) { + _showCursor.dispose(); + _disposeShowCursor = false; + } _showCursor = value; if (attached) { _showHideCursor(); diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index b1b51f021a4..0f7308f699b 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.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 'clipboard_utils.dart'; import 'editable_text_utils.dart'; @@ -88,18 +89,26 @@ void main() { final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder(delegate: delegate); + final TextEditingController controller = TextEditingController(); + addTearDown(controller.dispose); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: provider.buildGestureDetector( behavior: HitTestBehavior.translucent, - child: FakeEditableText(key: editableTextKey), + child: FakeEditableText( + key: editableTextKey, + controller: controller, + focusNode: focusNode, + ), ), ), ); } - testWidgets('a series of taps all call onTaps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a series of taps all call onTaps', (WidgetTester tester) async { await pumpGestureDetector(tester); await tester.tapAt(const Offset(200, 200)); await tester.pump(const Duration(milliseconds: 150)); @@ -115,7 +124,7 @@ void main() { expect(tapCount, 6); }); - testWidgets('in a series of rapid taps, onTapDown, onDoubleTapDown, and onTripleTapDown alternate', (WidgetTester tester) async { + testWidgetsWithLeakTracking('in a series of rapid taps, onTapDown, onDoubleTapDown, and onTripleTapDown alternate', (WidgetTester tester) async { await pumpGestureDetector(tester); await tester.tapAt(const Offset(200, 200)); await tester.pump(const Duration(milliseconds: 50)); @@ -151,7 +160,7 @@ void main() { expect(tapCount, 7); }); - testWidgets('quick tap-tap-hold is a double tap down', (WidgetTester tester) async { + testWidgetsWithLeakTracking('quick tap-tap-hold is a double tap down', (WidgetTester tester) async { await pumpGestureDetector(tester); await tester.tapAt(const Offset(200, 200)); await tester.pump(const Duration(milliseconds: 50)); @@ -177,7 +186,7 @@ void main() { expect(singleLongTapStartCount, 0); }); - testWidgets('a very quick swipe is ignored', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a very quick swipe is ignored', (WidgetTester tester) async { await pumpGestureDetector(tester); final TestGesture gesture = await tester.startGesture(const Offset(200, 200)); await tester.pump(const Duration(milliseconds: 20)); @@ -203,7 +212,7 @@ void main() { expect(singleLongTapStartCount, 0); }); - testWidgets('a slower swipe has a tap down and a canceled tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a slower swipe has a tap down and a canceled tap', (WidgetTester tester) async { await pumpGestureDetector(tester); final TestGesture gesture = await tester.startGesture(const Offset(200, 200)); await tester.pump(const Duration(milliseconds: 120)); @@ -216,7 +225,7 @@ void main() { expect(singleLongTapStartCount, 0); }); - testWidgets('a force press initiates a force press', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a force press initiates a force press', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -298,7 +307,7 @@ void main() { expect(forcePressStartCount, 4); }); - testWidgets('a tap and then force press initiates a force press and not a double tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a tap and then force press initiates a force press and not a double tap', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -351,7 +360,7 @@ void main() { expect(doubleTapDownCount, 0); }); - testWidgets('a long press from a touch device is recognized as a long single tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a long press from a touch device is recognized as a long single tap', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -368,7 +377,7 @@ void main() { expect(singleLongTapStartCount, 1); }); - testWidgets('a long press from a mouse is just a tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a long press from a mouse is just a tap', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -386,7 +395,7 @@ void main() { expect(singleLongTapStartCount, 0); }); - testWidgets('a touch drag is recognized for text selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a touch drag is recognized for text selection', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -408,7 +417,7 @@ void main() { expect(dragEndCount, 1); }); - testWidgets('a mouse drag is recognized for text selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a mouse drag is recognized for text selection', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -433,7 +442,7 @@ void main() { expect(dragEndCount, 1); }); - testWidgets('a slow mouse drag is still recognized for text selection', (WidgetTester tester) async { + testWidgetsWithLeakTracking('a slow mouse drag is still recognized for text selection', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -458,7 +467,7 @@ void main() { expect(dragEndCount, 1); }); - testWidgets('test TextSelectionGestureDetectorBuilder long press on Apple Platforms - focused renderEditable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder long press on Apple Platforms - focused renderEditable', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); @@ -477,7 +486,7 @@ void main() { expect(renderEditable.lastCause, SelectionChangedCause.longPress); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('test TextSelectionGestureDetectorBuilder long press on iOS - renderEditable not focused', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder long press on iOS - renderEditable not focused', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); @@ -495,7 +504,7 @@ void main() { expect(renderEditable.lastCause, SelectionChangedCause.longPress); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); - testWidgets('test TextSelectionGestureDetectorBuilder long press on non-Apple Platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder long press on non-Apple Platforms', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final TestGesture gesture = await tester.startGesture( const Offset(200.0, 200.0), @@ -512,7 +521,7 @@ void main() { expect(renderEditable.lastCause, SelectionChangedCause.longPress); }, variant: TargetPlatformVariant.all(excluding: { TargetPlatform.iOS, TargetPlatform.macOS })); - testWidgets('TextSelectionGestureDetectorBuilder right click Apple platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSelectionGestureDetectorBuilder right click Apple platforms', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80119 await pumpTextSelectionGestureDetectorBuilder(tester); @@ -557,7 +566,7 @@ void main() { variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS }), ); - testWidgets('TextSelectionGestureDetectorBuilder right click non-Apple platforms', (WidgetTester tester) async { + testWidgetsWithLeakTracking('TextSelectionGestureDetectorBuilder right click non-Apple platforms', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80119 await pumpTextSelectionGestureDetectorBuilder(tester); @@ -607,7 +616,7 @@ void main() { variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }), ); - testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final TestGesture gesture = await tester.startGesture( const Offset(200.0, 200.0), @@ -634,7 +643,7 @@ void main() { } }, variant: TargetPlatformVariant.all()); - testWidgets('test TextSelectionGestureDetectorBuilder toggles toolbar on single tap on previous selection iOS', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder toggles toolbar on single tap on previous selection iOS', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); @@ -666,7 +675,7 @@ void main() { }, variant: TargetPlatformVariant.all()); - testWidgets('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on Android', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on Android', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); @@ -686,7 +695,7 @@ void main() { }, variant: const TargetPlatformVariant({ TargetPlatform.android })); - testWidgets('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on iOS if word misspelled and text selection toolbar on additonal taps', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on iOS if word misspelled and text selection toolbar on additonal taps', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final FakeEditableTextState state = tester.state(find.byType(FakeEditableText)); final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); @@ -727,7 +736,7 @@ void main() { expect(state.toggleToolbarCalled, isTrue); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); - testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final TestGesture gesture = await tester.startGesture( const Offset(200.0, 200.0), @@ -747,7 +756,7 @@ void main() { expect(renderEditable.lastCause, SelectionChangedCause.doubleTap); }); - testWidgets('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final TestGesture gesture = await tester.createGesture(); await gesture.downWithCustomEvent( @@ -774,7 +783,7 @@ void main() { expect(renderEditable.selectWordsInRangeCalled, isTrue); }); - testWidgets('Mouse drag does not show handles nor toolbar', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse drag does not show handles nor toolbar', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/69001 await tester.pumpWidget( const MaterialApp( @@ -798,11 +807,12 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); }); - testWidgets('Mouse drag selects and cannot drag cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse drag selects and cannot drag cursor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/102928 final TextEditingController controller = TextEditingController( text: 'I love flutter!', ); + addTearDown(controller.dispose); final GlobalKey editableTextKey = GlobalKey(); final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( editableTextKey: editableTextKey, @@ -811,6 +821,8 @@ void main() { ); final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder(delegate: delegate); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -819,7 +831,7 @@ void main() { child: EditableText( key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -861,11 +873,12 @@ void main() { expect(controller.selection.extentOffset, 10); }); - testWidgets('Touch drag moves the cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Touch drag moves the cursor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/102928 final TextEditingController controller = TextEditingController( text: 'I love flutter!', ); + addTearDown(controller.dispose); final GlobalKey editableTextKey = GlobalKey(); final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( editableTextKey: editableTextKey, @@ -874,6 +887,8 @@ void main() { ); final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder(delegate: delegate); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -882,7 +897,7 @@ void main() { child: EditableText( key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -917,11 +932,12 @@ void main() { expect(controller.selection.baseOffset, 10); }); - testWidgets('Stylus drag moves the cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Stylus drag moves the cursor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/102928 final TextEditingController controller = TextEditingController( text: 'I love flutter!', ); + addTearDown(controller.dispose); final GlobalKey editableTextKey = GlobalKey(); final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( editableTextKey: editableTextKey, @@ -930,6 +946,8 @@ void main() { ); final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder(delegate: delegate); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -938,7 +956,7 @@ void main() { child: EditableText( key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -973,11 +991,12 @@ void main() { expect(controller.selection.baseOffset, 10); }); - testWidgets('Drag of unknown type moves the cursor', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Drag of unknown type moves the cursor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/102928 final TextEditingController controller = TextEditingController( text: 'I love flutter!', ); + addTearDown(controller.dispose); final GlobalKey editableTextKey = GlobalKey(); final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( editableTextKey: editableTextKey, @@ -986,6 +1005,8 @@ void main() { ); final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder(delegate: delegate); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -994,7 +1015,7 @@ void main() { child: EditableText( key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -1029,13 +1050,15 @@ void main() { expect(controller.selection.baseOffset, 10); }); - testWidgets('test TextSelectionGestureDetectorBuilder drag with RenderEditable viewport offset change', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder drag with RenderEditable viewport offset change', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester); final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); // Reconfigure the RenderEditable for multi-line. renderEditable.maxLines = null; - renderEditable.offset = ViewportOffset.fixed(20.0); + final ViewportOffset offset1 = ViewportOffset.fixed(20.0); + addTearDown(offset1.dispose); + renderEditable.offset = offset1; renderEditable.layout(const BoxConstraints.tightFor(width: 400, height: 300.0)); await tester.pumpAndSettle(); @@ -1053,7 +1076,9 @@ void main() { expect(renderEditable.selectPositionAtTo, const Offset(300.0, 200.0)); // Move the viewport offset (scroll). - renderEditable.offset = ViewportOffset.fixed(150.0); + final ViewportOffset offset2 = ViewportOffset.fixed(150.0); + addTearDown(offset2.dispose); + renderEditable.offset = offset2; renderEditable.layout(const BoxConstraints.tightFor(width: 400, height: 300.0)); await tester.pumpAndSettle(); @@ -1066,7 +1091,7 @@ void main() { expect(renderEditable.selectPositionAtTo, const Offset(300.0, 400.0)); }); - testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false); final TestGesture gesture = await tester.startGesture( const Offset(200.0, 200.0), @@ -1082,7 +1107,7 @@ void main() { expect(renderEditable.selectWordsInRangeCalled, isFalse); }); - testWidgets('test TextSelectionGestureDetectorBuilder mouse drag disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder mouse drag disabled', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false); final TestGesture gesture = await tester.startGesture( Offset.zero, @@ -1098,7 +1123,7 @@ void main() { expect(renderEditable.selectPositionAtCalled, isFalse); }); - testWidgets('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async { + testWidgetsWithLeakTracking('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester, forcePressEnabled: false); final TestGesture gesture = await tester.createGesture(); await gesture.downWithCustomEvent( @@ -1120,8 +1145,9 @@ void main() { }); // Regression test for https://github.com/flutter/flutter/issues/37032. - testWidgets("selection handle's GestureDetector should not cover the entire screen", (WidgetTester tester) async { + testWidgetsWithLeakTracking("selection handle's GestureDetector should not cover the entire screen", (WidgetTester tester) async { final TextEditingController controller = TextEditingController(text: 'a'); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( @@ -1189,6 +1215,9 @@ void main() { ), )); + final FakeClipboardStatusNotifier clipboardStatus = FakeClipboardStatusNotifier(); + addTearDown(clipboardStatus.dispose); + return SelectionOverlay( context: tester.element(find.byKey(column)), onSelectionHandleTapped: onSelectionHandleTapped, @@ -1204,7 +1233,7 @@ void main() { onEndHandleDragStart: onEndDragStart, onEndHandleDragUpdate: onEndDragUpdate, onEndHandleDragEnd: onEndDragEnd, - clipboardStatus: FakeClipboardStatusNotifier(), + clipboardStatus: clipboardStatus, selectionDelegate: FakeTextSelectionDelegate(), selectionControls: selectionControls, selectionEndpoints: const [], @@ -1213,7 +1242,7 @@ void main() { ); } - testWidgets('can show and hide handles', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can show and hide handles', (WidgetTester tester) async { final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); final SelectionOverlay selectionOverlay = await pumpApp( tester, @@ -1256,9 +1285,12 @@ void main() { expect(find.byKey(spy.leftHandleKey), findsNothing); expect(find.byKey(spy.rightHandleKey), findsNothing); expect(find.byKey(spy.toolBarKey), findsNothing); + + selectionOverlay.dispose(); + await tester.pumpAndSettle(); }); - testWidgets('only paints one collapsed handle', (WidgetTester tester) async { + testWidgetsWithLeakTracking('only paints one collapsed handle', (WidgetTester tester) async { final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); final SelectionOverlay selectionOverlay = await pumpApp( tester, @@ -1276,9 +1308,12 @@ void main() { expect(find.byKey(spy.leftHandleKey), findsNothing); expect(find.byKey(spy.rightHandleKey), findsNothing); expect(find.byKey(spy.collapsedHandleKey), findsOneWidget); + + selectionOverlay.dispose(); + await tester.pumpAndSettle(); }); - testWidgets('can change handle parameter', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can change handle parameter', (WidgetTester tester) async { final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); final SelectionOverlay selectionOverlay = await pumpApp( tester, @@ -1310,9 +1345,12 @@ void main() { rightHandle = tester.widget(find.byKey(spy.rightHandleKey)) as Text; expect(leftHandle.data, 'height 13'); expect(rightHandle.data, 'height 12'); + + selectionOverlay.dispose(); + await tester.pumpAndSettle(); }); - testWidgets('can trigger selection handle onTap', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can trigger selection handle onTap', (WidgetTester tester) async { bool selectionHandleTapped = false; void handleTapped() => selectionHandleTapped = true; final TextSelectionControlsSpy spy = TextSelectionControlsSpy(); @@ -1342,9 +1380,12 @@ void main() { selectionHandleTapped = false; await tester.tap(find.byKey(spy.rightHandleKey)); expect(selectionHandleTapped, isTrue); + + selectionOverlay.dispose(); + await tester.pumpAndSettle(); }); - testWidgets('can trigger selection handle drag', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can trigger selection handle drag', (WidgetTester tester) async { DragStartDetails? startDragStartDetails; DragUpdateDetails? startDragUpdateDetails; DragEndDetails? startDragEndDetails; @@ -1413,9 +1454,12 @@ void main() { await gesture2.up(); await tester.pump(const Duration(milliseconds: 20)); expect(endDragEndDetails, isNotNull); + + selectionOverlay.dispose(); + await tester.pumpAndSettle(); }); - testWidgets('can show magnifier when no handles exist', (WidgetTester tester) async { + testWidgetsWithLeakTracking('can show magnifier when no handles exist', (WidgetTester tester) async { final GlobalKey magnifierKey = GlobalKey(); final SelectionOverlay selectionOverlay = await pumpApp( tester, @@ -1442,6 +1486,9 @@ void main() { expect(tester.takeException(), isNull); expect(find.byKey(magnifierKey), findsOneWidget); + + selectionOverlay.dispose(); + await tester.pumpAndSettle(); }); }); @@ -1495,11 +1542,12 @@ void main() { }); }); - testWidgets('Mouse edge scrolling works in an outer scrollable', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse edge scrolling works in an outer scrollable', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/102484 final TextEditingController controller = TextEditingController( text: 'I love flutter!\n' * 8, ); + addTearDown(controller.dispose); final GlobalKey editableTextKey = GlobalKey(); final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( editableTextKey: editableTextKey, @@ -1508,11 +1556,14 @@ void main() { ); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); const double kLineHeight = 16.0; final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder( delegate: delegate, ); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -1527,7 +1578,7 @@ void main() { child: EditableText( key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -1572,11 +1623,12 @@ void main() { expect(scrollController.position.pixels, scrollController.position.maxScrollExtent); }); - testWidgets('Mouse edge scrolling works with both an outer scrollable and scrolling in the EditableText', (WidgetTester tester) async { + testWidgetsWithLeakTracking('Mouse edge scrolling works with both an outer scrollable and scrolling in the EditableText', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/102484 final TextEditingController controller = TextEditingController( text: 'I love flutter!\n' * 8, ); + addTearDown(controller.dispose); final GlobalKey editableTextKey = GlobalKey(); final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate( editableTextKey: editableTextKey, @@ -1585,11 +1637,14 @@ void main() { ); final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); const double kLineHeight = 16.0; final TextSelectionGestureDetectorBuilder provider = TextSelectionGestureDetectorBuilder( delegate: delegate, ); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( @@ -1604,7 +1659,7 @@ void main() { child: EditableText( key: editableTextKey, controller: controller, - focusNode: FocusNode(), + focusNode: focusNode, backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -1668,9 +1723,11 @@ class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGe } class FakeEditableText extends EditableText { - FakeEditableText({super.key}): super( - controller: TextEditingController(), - focusNode: FocusNode(), + FakeEditableText({ + required super.controller, + required super.focusNode, + super.key, + }): super( backgroundCursorColor: Colors.white, cursorColor: Colors.white, style: const TextStyle(), @@ -1738,7 +1795,13 @@ class FakeEditable extends LeafRenderObjectWidget { } class FakeRenderEditable extends RenderEditable { - FakeRenderEditable(EditableTextState delegate) : super( + FakeRenderEditable(EditableTextState delegate) + : this._(delegate, ViewportOffset.fixed(10.0)); + + FakeRenderEditable._( + EditableTextState delegate, + this._offset, + ) : super( text: const TextSpan( style: TextStyle(height: 1.0, fontSize: 10.0), text: 'placeholder', @@ -1749,7 +1812,7 @@ class FakeRenderEditable extends RenderEditable { textAlign: TextAlign.start, textDirection: TextDirection.ltr, locale: const Locale('en', 'US'), - offset: ViewportOffset.fixed(10.0), + offset: _offset, textSelectionDelegate: delegate, selection: const TextSelection.collapsed( offset: 0, @@ -1758,6 +1821,8 @@ class FakeRenderEditable extends RenderEditable { SelectionChangedCause? lastCause; + ViewportOffset _offset; + bool selectWordsInRangeCalled = false; @override void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) { @@ -1804,6 +1869,12 @@ class FakeRenderEditable extends RenderEditable { @override bool hasFocus = false; + + @override + void dispose() { + _offset.dispose(); + super.dispose(); + } } class TextSelectionControlsSpy extends TextSelectionControls { diff --git a/packages/flutter/test/widgets/text_selection_toolbar_layout_delegate_test.dart b/packages/flutter/test/widgets/text_selection_toolbar_layout_delegate_test.dart index 1fc67c761d6..9ccec43ba8b 100644 --- a/packages/flutter/test/widgets/text_selection_toolbar_layout_delegate_test.dart +++ b/packages/flutter/test/widgets/text_selection_toolbar_layout_delegate_test.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { - testWidgets('positions itself at anchorAbove if it fits', (WidgetTester tester) async { + testWidgetsWithLeakTracking('positions itself at anchorAbove if it fits', (WidgetTester tester) async { late StateSetter setState; const double height = 43.0; const double anchorBelowY = 500.0;