salim-lachdhaf / dropdown_search

Simple and robust Dropdown with item search feature, making it possible to use an offline item list or filtering URL for easy customization.
MIT License
346 stars 334 forks source link

fix: do not closePopup on dispose #690

Closed Zigotote closed 1 month ago

Zigotote commented 1 month ago

Using the dropdownMenu inside tests makes the test fail because closePopup is called during test tearDown, making Navigor.of(context).pop() throwing an error.

Full stack trace is here :

``` ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following assertion was thrown while finalizing the widget tree: Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method. When the exception was thrown, this was the stack: #0 Element._debugCheckStateIsActiveForAncestorLookup. (package:flutter/src/widgets/framework.dart:4873:9) #1 Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4887:6) #2 Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4958:12) #3 Navigator.of (package:flutter/src/widgets/navigator.dart:2781:40) #4 Navigator.pop (package:flutter/src/widgets/navigator.dart:2665:15) #5 DropdownSearchPopupState.closePopup (package:dropdown_search/src/widgets/dropdown_search_popup.dart:781:34) #6 DropdownSearchState.dispose (package:dropdown_search/dropdown_search.dart:773:34) #7 StatefulElement.unmount (package:flutter/src/widgets/framework.dart:5826:11) #8 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2077:13) #9 _InactiveElements._unmount. (package:flutter/src/widgets/framework.dart:2075:7) #10 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6886:14) ════════════════════════════════════════════════════════════════════════════════════════════════════ ══╡ EXCEPTION CAUGHT BY SCHEDULER LIBRARY ╞═════════════════════════════════════════════════════════ The following message was thrown: An animation is still running even after the widget tree was disposed. There were 3 transient callbacks left. The stack traces for when they were registered are as follows: ── callback 25 ── #2 SchedulerBinding.scheduleFrameCallback (package:flutter/src/scheduler/binding.dart:611:49) #3 Ticker.scheduleTick (package:flutter/src/scheduler/ticker.dart:277:46) #4 Ticker.start (package:flutter/src/scheduler/ticker.dart:183:7) #5 AnimationController._startSimulation (package:flutter/src/animation/animation_controller.dart:820:42) #6 AnimationController._animateToInternal (package:flutter/src/animation/animation_controller.dart:691:12) #7 AnimationController.forward (package:flutter/src/animation/animation_controller.dart:500:12) #8 _InputDecoratorState.didUpdateWidget (package:flutter/src/material/input_decorator.dart:1946:34) #9 StatefulElement.update (package:flutter/src/widgets/framework.dart:5789:55) #10 Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15) #11 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16) #12 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5780:11) #13 Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7) #14 StatefulElement.update (package:flutter/src/widgets/framework.dart:5803:5) #15 Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15) #16 SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6907:14) #17 Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15) #18 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16) #19 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5780:11) #20 Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7) #21 StatefulElement.update (package:flutter/src/widgets/framework.dart:5803:5) ════════════════════════════════════════════════════════════════════════════════════════════════════ Pending timers: Timer (duration: 0:00:00.500000, periodic: false), created: #0 new FakeTimer._ (package:fake_async/fake_async.dart:308:62) #1 FakeAsync._createTimer (package:fake_async/fake_async.dart:252:27) #2 FakeAsync.run. (package:fake_async/fake_async.dart:185:19) #5 _throttle. (package:flutter/src/widgets/undo_history.dart:499:13) #6 UndoHistoryState._push (package:flutter/src/widgets/undo_history.dart:197:36) #7 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:437:24) #8 ValueNotifier.value= (package:flutter/src/foundation/change_notifier.dart:559:5) #9 TextEditingController.value= (package:flutter/src/widgets/editable_text.dart:266:11) #10 TextEditingController.selection= (package:flutter/src/widgets/editable_text.dart:321:5) #11 EditableTextState._handleSelectionChanged (package:flutter/src/widgets/editable_text.dart:3946:23) #12 EditableTextState._handleFocusChanged (package:flutter/src/widgets/editable_text.dart:4353:9) #13 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:437:24) #14 FocusNode._notify (package:flutter/src/widgets/focus_manager.dart:1090:5) #15 FocusManager.applyFocusChangesIfNeeded (package:flutter/src/widgets/focus_manager.dart:1871:12) #22 FakeAsync.flushMicrotasks (package:fake_async/fake_async.dart:197:32) #23 AutomatedTestWidgetsFlutterBinding.pump. (package:flutter_test/src/binding.dart:1290:26) #26 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:74:41) #27 AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1275:27) #28 WidgetTester.pump. (package:flutter_test/src/widget_tester.dart:667:53) #31 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:74:41) #32 WidgetTester.pump (package:flutter_test/src/widget_tester.dart:667:27) #33 WidgetTester.showKeyboard. (package:flutter_test/src/widget_tester.dart:1135:13) #36 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:74:41) #37 WidgetTester.showKeyboard (package:flutter_test/src/widget_tester.dart:1123:27) #38 WidgetTester.enterText. (package:flutter_test/src/widget_tester.dart:1159:13) #41 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:74:41) #42 WidgetTester.enterText (package:flutter_test/src/widget_tester.dart:1158:27) #43 main. (file:///home/githubrunner2/actions-runner/_work/my-app/test/components/dropdown_test.dart:21:18) #44 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15) #45 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5) (elided 17 frames from dart:async and package:stack_trace) Timer (duration: 0:00:01.000000, periodic: false), created: #0 new FakeTimer._ (package:fake_async/fake_async.dart:308:62) #1 FakeAsync._createTimer (package:fake_async/fake_async.dart:252:27) #2 FakeAsync.run. (package:fake_async/fake_async.dart:185:19) #5 DropdownSearchPopupState.searchBoxControllerListener (package:dropdown_search/src/widgets/dropdown_search_popup.dart:57:17) #6 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:437:24) #7 ValueNotifier.value= (package:flutter/src/foundation/change_notifier.dart:559:5) #8 TextEditingController.value= (package:flutter/src/widgets/editable_text.dart:266:11) #9 EditableTextState._value= (package:flutter/src/widgets/editable_text.dart:3531:23) #10 EditableTextState._formatAndSetValue (package:flutter/src/widgets/editable_text.dart:4183:5) #11 EditableTextState.updateEditingValue (package:flutter/src/widgets/editable_text.dart:3246:7) #12 TextInput._updateEditingValue (package:flutter/src/services/text_input.dart:2067:43) #13 TextInput._handleTextInputInvocation (package:flutter/src/services/text_input.dart:1903:29) #14 TextInput._loudlyHandleTextInputInvocation (package:flutter/src/services/text_input.dart:1803:20) #15 MethodChannel._handleAsMethodCall (package:flutter/src/services/platform_channel.dart:571:55) #16 MethodChannel.setMethodCallHandler. (package:flutter/src/services/platform_channel.dart:564:34) #17 TestDefaultBinaryMessenger.handlePlatformMessage (package:flutter_test/src/test_default_binary_messenger.dart:99:42) #18 TestTextInput.updateEditingValue (package:flutter_test/src/test_text_input.dart:204:71) #19 TestTextInput.enterText (package:flutter_test/src/test_text_input.dart:182:5) #20 WidgetTester.enterText. (package:flutter_test/src/widget_tester.dart:1160:21) #21 TestAsyncUtils.guard. (package:flutter_test/src/test_async_utils.dart:120:7) #22 main. (package:flutter_test/src/widget_tester.dart:189:15) #23 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15) #24 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5) (elided 3 frames from dart:async and package:stack_trace) Timer (duration: 0:00:00.500000, periodic: true), created: #0 new FakeTimer._ (package:fake_async/fake_async.dart:308:62) #1 FakeAsync._createTimer (package:fake_async/fake_async.dart:252:27) #2 FakeAsync.run. (package:fake_async/fake_async.dart:187:19) #5 EditableTextState._startCursorBlink (package:flutter/src/widgets/editable_text.dart:4275:28) #6 EditableTextState.updateEditingValue (package:flutter/src/widgets/editable_text.dart:3252:7) #7 TextInput._updateEditingValue (package:flutter/src/services/text_input.dart:2067:43) #8 TextInput._handleTextInputInvocation (package:flutter/src/services/text_input.dart:1903:29) #9 TextInput._loudlyHandleTextInputInvocation (package:flutter/src/services/text_input.dart:1803:20) #10 MethodChannel._handleAsMethodCall (package:flutter/src/services/platform_channel.dart:571:55) #11 MethodChannel.setMethodCallHandler. (package:flutter/src/services/platform_channel.dart:564:34) #12 TestDefaultBinaryMessenger.handlePlatformMessage (package:flutter_test/src/test_default_binary_messenger.dart:99:42) #13 TestTextInput.updateEditingValue (package:flutter_test/src/test_text_input.dart:204:71) #14 TestTextInput.enterText (package:flutter_test/src/test_text_input.dart:182:5) #15 WidgetTester.enterText. (package:flutter_test/src/widget_tester.dart:1160:21) #16 TestAsyncUtils.guard. (package:flutter_test/src/test_async_utils.dart:120:7) #17 main. (package:flutter_test/src/widget_tester.dart:189:15) #18 testWidgets.. (package:flutter_test/src/widget_tester.dart:189:15) #19 TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5) (elided 3 frames from dart:async and package:stack_trace) ```
salim-lachdhaf commented 1 month ago

Thank you for PR. Could you past a simple reproducible test exmaple that throws exception ?

Zigotote commented 1 month ago

For example this test works with my commit, but fails on master

import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

main() {
  testWidgets("should search item", (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: DropdownSearch(
            items: (_, __) => ["apple", "banana", "pineapple"],
            compareFn: (item, selectedItem) => item == selectedItem,
            popupProps:
                const PopupPropsMultiSelection.menu(showSearchBox: true),
          ),
        ),
      ),
    );
    await tester.pumpAndSettle();

    // Open dropdown
    await tester.tap(find.byType(FormField));
    await tester.pump();

    // Search value
    await tester.enterText(find.byType(TextField), "ap");
    await tester.pumpAndSettle();

    expect(find.text("apple"), findsOneWidget);
    expect(find.text("pineapple"), findsOneWidget);
    expect(find.text("banana"), findsNothing);
  });
}
salim-lachdhaf commented 1 month ago

1 - There is no problem with the package. you have just to correct your test. 2- by adding your correction, you are making a huge regressions, i will just list some:

here is my test : ``` import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; main() { testWidgets("should search item", (tester) async { final key = GlobalKey(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownSearch( key: key, items: (_, __) => ["apple", "banana", "pineapple"], selectedItem: "banana", compareFn: (item, selectedItem) => item == selectedItem, popupProps: const PopupProps.menu(showSearchBox: true, searchDelay: Duration.zero), ), ), ), ); //check default selected item expect(find.text("banana"), findsOneWidget); //await tester.pumpAndSettle(); // Open dropdown await tester.tap(find.byKey(key)); await tester.pumpAndSettle(); //check items expect(find.text("apple"), findsOneWidget); expect(find.text("banana"), findsNWidgets(2)); //2 items: selected + item on popup expect(find.text("pineapple"), findsOneWidget); //select pineapple await tester.tap(find.text("pineapple")); await tester.pumpAndSettle(); //reopen dropdown await tester.tap(find.byKey(key)); await tester.pumpAndSettle(); // Search value await tester.enterText(find.byType(TextField), "ap"); await tester.pumpAndSettle(); expect(find.text("ap"), findsOneWidget); expect(find.text("pineapple"), findsNWidgets(2));//one in search and the selected one expect(find.text("apple"), findsOneWidget); expect(find.text("banana"), findsNothing); //select value await tester.tap(find.text("apple")); await tester.pumpAndSettle(); expect(find.text("apple"), findsOneWidget); }); } ```
and this your test corrected ``` import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; main() { testWidgets("should search item", (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DropdownSearch( key: Key('dropdown'), items: (_, __) => ["apple", "banana", "pineapple"], popupProps: const PopupProps.menu( showSearchBox: true, searchDelay: Duration.zero /*other wise you have to add a delay in your test*/), ), ), ), ); // Open dropdown await tester.tap(find.byKey(Key('dropdown') /*use key i think it's better*/)); await tester.pumpAndSettle(); // Search value await tester.enterText(find.byType(TextField), "ap"); await tester.pumpAndSettle(); expect(find.text("apple"), findsOneWidget); expect(find.text("pineapple"), findsOneWidget); expect(find.text("banana"), findsNothing); //close popup !! to prevent memory leaks and prevent crash because the popup is NO MORE ATTACHED to the parent widget await tester.tap(find.text("apple")); await tester.pumpAndSettle(); }); } ```
sgshy1995 commented 1 month ago

Using the dropdownMenu inside tests makes the test fail because closePopup is called during test tearDown, making Navigor.of(context).pop() throwing an error.

Full stack trace is here :

@Zigotote This is a problem. When you click popup frequently and quickly, and then click external outside close, you will find that the page will exit abnormally.

Navigator.pop(context) may be called abnormally.

Zigotote commented 1 month ago

Thank you for all your returns. I didn't thought about checking how the Flutter team handled the tests of this component. I added your code and it fixed my tests. Thanks for your time