// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'leak_tracking.dart'; class TestNotifier extends ChangeNotifier { void notify() { notifyListeners(); } bool get isListenedTo => hasListeners; } class HasListenersTester<T> extends ValueNotifier<T> { HasListenersTester(super.value); bool get testHasListeners => hasListeners; } class A { bool result = false; void test() { result = true; } } class B extends A with ChangeNotifier { @override void test() { notifyListeners(); super.test(); } } class Counter with ChangeNotifier { int get value => _value; int _value = 0; set value(int value) { if (_value != value) { _value = value; notifyListeners(); } } void notify() { notifyListeners(); } } void main() { testWidgetsWithLeakTracking('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { final TestNotifier test = TestNotifier(); bool callbackDidFinish = false; void foo() { test.dispose(); callbackDidFinish = true; } test.addListener(foo); test.notify(); final AssertionError error = tester.takeException() as AssertionError; expect(error.toString().contains('dispose()'), isTrue); // Make sure it crashes during dispose call. expect(callbackDidFinish, isFalse); test.dispose(); }); testWidgetsWithLeakTracking('ChangeNotifier', (WidgetTester tester) async { final List<String> log = <String>[]; void listener() { log.add('listener'); } void listener1() { log.add('listener1'); } void listener2() { log.add('listener2'); } void badListener() { log.add('badListener'); throw ArgumentError(); } final TestNotifier test = TestNotifier(); test.addListener(listener); test.addListener(listener); test.notify(); expect(log, <String>['listener', 'listener']); log.clear(); test.removeListener(listener); test.notify(); expect(log, <String>['listener']); log.clear(); test.removeListener(listener); test.notify(); expect(log, <String>[]); log.clear(); test.removeListener(listener); test.notify(); expect(log, <String>[]); log.clear(); test.addListener(listener); test.notify(); expect(log, <String>['listener']); log.clear(); test.addListener(listener1); test.notify(); expect(log, <String>['listener', 'listener1']); log.clear(); test.addListener(listener2); test.notify(); expect(log, <String>['listener', 'listener1', 'listener2']); log.clear(); test.removeListener(listener1); test.notify(); expect(log, <String>['listener', 'listener2']); log.clear(); test.addListener(listener1); test.notify(); expect(log, <String>['listener', 'listener2', 'listener1']); log.clear(); test.addListener(badListener); test.notify(); expect(log, <String>['listener', 'listener2', 'listener1', 'badListener']); expect(tester.takeException(), isArgumentError); log.clear(); test.addListener(listener1); test.removeListener(listener); test.removeListener(listener1); test.removeListener(listener2); test.addListener(listener2); test.notify(); expect(log, <String>['badListener', 'listener1', 'listener2']); expect(tester.takeException(), isArgumentError); log.clear(); test.dispose(); }); test('ChangeNotifier with mutating listener', () { final TestNotifier test = TestNotifier(); final List<String> log = <String>[]; void listener1() { log.add('listener1'); } void listener3() { log.add('listener3'); } void listener4() { log.add('listener4'); } void listener2() { log.add('listener2'); test.removeListener(listener1); test.removeListener(listener3); test.addListener(listener4); } test.addListener(listener1); test.addListener(listener2); test.addListener(listener3); test.notify(); expect(log, <String>['listener1', 'listener2']); log.clear(); test.notify(); expect(log, <String>['listener2', 'listener4']); log.clear(); test.notify(); expect(log, <String>['listener2', 'listener4', 'listener4']); log.clear(); }); test('During notifyListeners, a listener was added and removed immediately', () { final TestNotifier source = TestNotifier(); final List<String> log = <String>[]; void listener3() { log.add('listener3'); } void listener2() { log.add('listener2'); } void listener1() { log.add('listener1'); source.addListener(listener2); source.removeListener(listener2); source.addListener(listener3); } source.addListener(listener1); source.notify(); expect(log, <String>['listener1']); }); test( 'If a listener in the middle of the list of listeners removes itself, ' 'notifyListeners still notifies all listeners', () { final TestNotifier source = TestNotifier(); final List<String> log = <String>[]; void selfRemovingListener() { log.add('selfRemovingListener'); source.removeListener(selfRemovingListener); } void listener1() { log.add('listener1'); } source.addListener(listener1); source.addListener(selfRemovingListener); source.addListener(listener1); source.notify(); expect(log, <String>['listener1', 'selfRemovingListener', 'listener1']); }, ); test('If the first listener removes itself, notifyListeners still notify all listeners', () { final TestNotifier source = TestNotifier(); final List<String> log = <String>[]; void selfRemovingListener() { log.add('selfRemovingListener'); source.removeListener(selfRemovingListener); } void listener1() { log.add('listener1'); } source.addListener(selfRemovingListener); source.addListener(listener1); source.notifyListeners(); expect(log, <String>['selfRemovingListener', 'listener1']); }); test('Merging change notifiers', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); final TestNotifier source3 = TestNotifier(); final List<String> log = <String>[]; final Listenable merged = Listenable.merge(<Listenable>[source1, source2]); void listener1() { log.add('listener1'); } void listener2() { log.add('listener2'); } merged.addListener(listener1); source1.notify(); source2.notify(); source3.notify(); expect(log, <String>['listener1', 'listener1']); log.clear(); merged.removeListener(listener1); source1.notify(); source2.notify(); source3.notify(); expect(log, isEmpty); log.clear(); merged.addListener(listener1); merged.addListener(listener2); source1.notify(); source2.notify(); source3.notify(); expect(log, <String>['listener1', 'listener2', 'listener1', 'listener2']); log.clear(); }); test('Merging change notifiers ignores null', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); final List<String> log = <String>[]; final Listenable merged = Listenable.merge(<Listenable?>[null, source1, null, source2, null]); void listener() { log.add('listener'); } merged.addListener(listener); source1.notify(); source2.notify(); expect(log, <String>['listener', 'listener']); log.clear(); }); test('Can remove from merged notifier', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); final List<String> log = <String>[]; final Listenable merged = Listenable.merge(<Listenable>[source1, source2]); void listener() { log.add('listener'); } merged.addListener(listener); source1.notify(); source2.notify(); expect(log, <String>['listener', 'listener']); log.clear(); merged.removeListener(listener); source1.notify(); source2.notify(); expect(log, isEmpty); }); test('Cannot use a disposed ChangeNotifier except for remove listener', () { final TestNotifier source = TestNotifier(); source.dispose(); expect(() { source.addListener(() {}); }, throwsFlutterError); expect(() { source.dispose(); }, throwsFlutterError); expect(() { source.notify(); }, throwsFlutterError); }); test('Can remove listener on a disposed ChangeNotifier', () { final TestNotifier source = TestNotifier(); FlutterError? error; try { source.dispose(); source.removeListener(() {}); } on FlutterError catch (e) { error = e; } expect(error, isNull); }); test('Can check hasListener on a disposed ChangeNotifier', () { final HasListenersTester<int> source = HasListenersTester<int>(0); source.addListener(() { }); expect(source.testHasListeners, isTrue); FlutterError? error; try { source.dispose(); expect(source.testHasListeners, isFalse); } on FlutterError catch (e) { error = e; } expect(error, isNull); }); test('Value notifier', () { final ValueNotifier<double> notifier = ValueNotifier<double>(2.0); final List<double> log = <double>[]; void listener() { log.add(notifier.value); } notifier.addListener(listener); notifier.value = 3.0; expect(log, equals(<double>[3.0])); log.clear(); notifier.value = 3.0; expect(log, isEmpty); }); test('Listenable.merge toString', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); Listenable listenableUnderTest = Listenable.merge(<Listenable>[]); expect(listenableUnderTest.toString(), 'Listenable.merge([])'); listenableUnderTest = Listenable.merge(<Listenable?>[null]); expect(listenableUnderTest.toString(), 'Listenable.merge([null])'); listenableUnderTest = Listenable.merge(<Listenable>[source1]); expect( listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier'])", ); listenableUnderTest = Listenable.merge(<Listenable>[source1, source2]); expect( listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])", ); listenableUnderTest = Listenable.merge(<Listenable?>[null, source2]); expect( listenableUnderTest.toString(), "Listenable.merge([null, Instance of 'TestNotifier'])", ); }); test('Listenable.merge does not leak', () { // Regression test for https://github.com/flutter/flutter/issues/25163. final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); void fakeListener() {} final Listenable listenableUnderTest = Listenable.merge(<Listenable>[source1, source2]); expect(source1.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse); listenableUnderTest.addListener(fakeListener); expect(source1.isListenedTo, isTrue); expect(source2.isListenedTo, isTrue); listenableUnderTest.removeListener(fakeListener); expect(source1.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse); }); test('hasListeners', () { final HasListenersTester<bool> notifier = HasListenersTester<bool>(true); expect(notifier.testHasListeners, isFalse); void test1() {} void test2() {} notifier.addListener(test1); expect(notifier.testHasListeners, isTrue); notifier.addListener(test1); expect(notifier.testHasListeners, isTrue); notifier.removeListener(test1); expect(notifier.testHasListeners, isTrue); notifier.removeListener(test1); expect(notifier.testHasListeners, isFalse); notifier.addListener(test1); expect(notifier.testHasListeners, isTrue); notifier.addListener(test2); expect(notifier.testHasListeners, isTrue); notifier.removeListener(test1); expect(notifier.testHasListeners, isTrue); notifier.removeListener(test2); expect(notifier.testHasListeners, isFalse); }); test('ChangeNotifier as a mixin', () { // We document that this is a valid way to use this class. final B b = B(); int notifications = 0; b.addListener(() { notifications += 1; }); expect(b.result, isFalse); expect(notifications, 0); b.test(); expect(b.result, isTrue); expect(notifications, 1); }); test('Throws FlutterError when disposed and called', () { final TestNotifier testNotifier = TestNotifier(); testNotifier.dispose(); FlutterError? error; try { testNotifier.dispose(); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect(error, isFlutterError); expect( error!.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' A TestNotifier was used after being disposed.\n' ' Once you have called dispose() on a TestNotifier, it can no\n' ' longer be used.\n', ), ); }); test('Calling debugAssertNotDisposed works as intended', () { final TestNotifier testNotifier = TestNotifier(); expect(ChangeNotifier.debugAssertNotDisposed(testNotifier), isTrue); testNotifier.dispose(); FlutterError? error; try { ChangeNotifier.debugAssertNotDisposed(testNotifier); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect(error, isFlutterError); expect( error!.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' A TestNotifier was used after being disposed.\n' ' Once you have called dispose() on a TestNotifier, it can no\n' ' longer be used.\n', ), ); }); test('notifyListener can be called recursively', () { final Counter counter = Counter(); final List<String> log = <String>[]; void listener1() { log.add('listener1'); if (counter.value < 0) { counter.value = 0; } } counter.addListener(listener1); counter.notify(); expect(log, <String>['listener1']); log.clear(); counter.value = 3; expect(log, <String>['listener1']); log.clear(); counter.value = -2; expect(log, <String>['listener1', 'listener1']); log.clear(); }); test('Remove Listeners while notifying on a list which will not resize', () { final TestNotifier test = TestNotifier(); final List<String> log = <String>[]; final List<VoidCallback> listeners = <VoidCallback>[]; void autoRemove() { // We remove 4 listeners. // We will end up with (13-4 = 9) listeners. test.removeListener(listeners[1]); test.removeListener(listeners[3]); test.removeListener(listeners[4]); test.removeListener(autoRemove); } test.addListener(autoRemove); // We add 12 more listeners. for (int i = 0; i < 12; i++) { void listener() { log.add('listener$i'); } listeners.add(listener); test.addListener(listener); } final List<int> remainingListenerIndexes = <int>[ 0, 2, 5, 6, 7, 8, 9, 10, 11, ]; final List<String> expectedLog = remainingListenerIndexes.map((int i) => 'listener$i').toList(); test.notify(); expect(log, expectedLog); log.clear(); // We expect to have the same result after the removal of previous listeners. test.notify(); expect(log, expectedLog); // We remove all other listeners. for (int i = 0; i < remainingListenerIndexes.length; i++) { test.removeListener(listeners[remainingListenerIndexes[i]]); } log.clear(); test.notify(); expect(log, <String>[]); }); }