change_notifier_test.dart 15.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11
// 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';

class TestNotifier extends ChangeNotifier {
  void notify() {
    notifyListeners();
  }
12 13

  bool get isListenedTo => hasListeners;
14 15
}

16
class HasListenersTester<T> extends ValueNotifier<T> {
17
  HasListenersTester(super.value);
18 19 20 21 22
  bool get testHasListeners => hasListeners;
}

class A {
  bool result = false;
23 24 25
  void test() {
    result = true;
  }
26 27 28 29 30 31 32 33 34 35
}

class B extends A with ChangeNotifier {
  @override
  void test() {
    notifyListeners();
    super.test();
  }
}

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
class Counter with ChangeNotifier {
  int get value => _value;
  int _value = 0;
  set value(int value) {
    if (_value != value) {
      _value = value;
      notifyListeners();
    }
  }

  void notify() {
    notifyListeners();
  }
}

51
void main() {
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
  testWidgets('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);
  });

68 69
  testWidgets('ChangeNotifier', (WidgetTester tester) async {
    final List<String> log = <String>[];
70 71 72 73 74 75 76 77 78 79 80 81
    void listener() {
      log.add('listener');
    }

    void listener1() {
      log.add('listener1');
    }

    void listener2() {
      log.add('listener2');
    }

82
    void badListener() {
83
      log.add('badListener');
84
      throw ArgumentError();
85
    }
86

87
    final TestNotifier test = TestNotifier();
88 89 90 91

    test.addListener(listener);
    test.addListener(listener);
    test.notify();
92
    expect(log, <String>['listener', 'listener']);
93 94 95 96
    log.clear();

    test.removeListener(listener);
    test.notify();
97
    expect(log, <String>['listener']);
98 99 100 101
    log.clear();

    test.removeListener(listener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
102
    expect(log, <String>[]);
103 104 105 106
    log.clear();

    test.removeListener(listener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
107
    expect(log, <String>[]);
108 109 110 111
    log.clear();

    test.addListener(listener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
112
    expect(log, <String>['listener']);
113 114 115 116
    log.clear();

    test.addListener(listener1);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
117
    expect(log, <String>['listener', 'listener1']);
118 119 120 121
    log.clear();

    test.addListener(listener2);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
122
    expect(log, <String>['listener', 'listener1', 'listener2']);
123 124 125 126
    log.clear();

    test.removeListener(listener1);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
127
    expect(log, <String>['listener', 'listener2']);
128 129 130 131
    log.clear();

    test.addListener(listener1);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
132
    expect(log, <String>['listener', 'listener2', 'listener1']);
133 134 135 136
    log.clear();

    test.addListener(badListener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
137
    expect(log, <String>['listener', 'listener2', 'listener1', 'badListener']);
138
    expect(tester.takeException(), isArgumentError);
139 140 141 142 143 144 145 146
    log.clear();

    test.addListener(listener1);
    test.removeListener(listener);
    test.removeListener(listener1);
    test.removeListener(listener2);
    test.addListener(listener2);
    test.notify();
147
    expect(log, <String>['badListener', 'listener1', 'listener2']);
148
    expect(tester.takeException(), isArgumentError);
149
    log.clear();
Kate Lovett's avatar
Kate Lovett committed
150
  });
151

152
  test('ChangeNotifier with mutating listener', () {
153
    final TestNotifier test = TestNotifier();
154 155
    final List<String> log = <String>[];

156 157 158 159 160 161 162 163 164 165 166 167
    void listener1() {
      log.add('listener1');
    }

    void listener3() {
      log.add('listener3');
    }

    void listener4() {
      log.add('listener4');
    }

168
    void listener2() {
169 170 171 172
      log.add('listener2');
      test.removeListener(listener1);
      test.removeListener(listener3);
      test.addListener(listener4);
173
    }
174 175 176 177 178

    test.addListener(listener1);
    test.addListener(listener2);
    test.addListener(listener3);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
179
    expect(log, <String>['listener1', 'listener2']);
180 181 182
    log.clear();

    test.notify();
Ian Hickson's avatar
Ian Hickson committed
183
    expect(log, <String>['listener2', 'listener4']);
184 185 186
    log.clear();

    test.notify();
187
    expect(log, <String>['listener2', 'listener4', 'listener4']);
Ian Hickson's avatar
Ian Hickson committed
188 189 190
    log.clear();
  });

191
  test('During notifyListeners, a listener was added and removed immediately', () {
192 193 194
    final TestNotifier source = TestNotifier();
    final List<String> log = <String>[];

195 196 197 198 199 200 201 202
    void listener3() {
      log.add('listener3');
    }

    void listener2() {
      log.add('listener2');
    }

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
    void listener1() {
      log.add('listener1');
      source.addListener(listener2);
      source.removeListener(listener2);
      source.addListener(listener3);
    }

    source.addListener(listener1);

    source.notify();

    expect(log, <String>['listener1']);
  });

  test(
218 219 220 221 222 223 224 225 226 227
    '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);
      }
228

229 230 231
      void listener1() {
        log.add('listener1');
      }
232

233 234 235
      source.addListener(listener1);
      source.addListener(selfRemovingListener);
      source.addListener(listener1);
236

237
      source.notify();
238

239 240 241
      expect(log, <String>['listener1', 'selfRemovingListener', 'listener1']);
    },
  );
242

243
  test('If the first listener removes itself, notifyListeners still notify all listeners', () {
244 245 246 247 248 249 250
    final TestNotifier source = TestNotifier();
    final List<String> log = <String>[];

    void selfRemovingListener() {
      log.add('selfRemovingListener');
      source.removeListener(selfRemovingListener);
    }
251

252 253 254 255 256 257 258 259 260 261 262 263
    void listener1() {
      log.add('listener1');
    }

    source.addListener(selfRemovingListener);
    source.addListener(listener1);

    source.notifyListeners();

    expect(log, <String>['selfRemovingListener', 'listener1']);
  });

264
  test('Merging change notifiers', () {
265 266 267
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
    final TestNotifier source3 = TestNotifier();
Ian Hickson's avatar
Ian Hickson committed
268 269
    final List<String> log = <String>[];

270
    final Listenable merged = Listenable.merge(<Listenable>[source1, source2]);
271 272 273 274 275 276 277
    void listener1() {
      log.add('listener1');
    }

    void listener2() {
      log.add('listener2');
    }
Ian Hickson's avatar
Ian Hickson committed
278

279
    merged.addListener(listener1);
Ian Hickson's avatar
Ian Hickson committed
280 281 282
    source1.notify();
    source2.notify();
    source3.notify();
283
    expect(log, <String>['listener1', 'listener1']);
Ian Hickson's avatar
Ian Hickson committed
284 285
    log.clear();

286
    merged.removeListener(listener1);
Ian Hickson's avatar
Ian Hickson committed
287 288 289 290
    source1.notify();
    source2.notify();
    source3.notify();
    expect(log, isEmpty);
291
    log.clear();
292 293 294 295 296 297 298 299 300 301

    merged.addListener(listener1);
    merged.addListener(listener2);
    source1.notify();
    source2.notify();
    source3.notify();
    expect(log, <String>['listener1', 'listener2', 'listener1', 'listener2']);
    log.clear();
  });

302
  test('Merging change notifiers ignores null', () {
303 304
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
305 306
    final List<String> log = <String>[];

307 308 309 310 311
    final Listenable merged =
        Listenable.merge(<Listenable?>[null, source1, null, source2, null]);
    void listener() {
      log.add('listener');
    }
312 313 314 315 316 317

    merged.addListener(listener);
    source1.notify();
    source2.notify();
    expect(log, <String>['listener', 'listener']);
    log.clear();
318
  });
319

320
  test('Can remove from merged notifier', () {
321 322
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
323 324
    final List<String> log = <String>[];

325
    final Listenable merged = Listenable.merge(<Listenable>[source1, source2]);
326 327 328
    void listener() {
      log.add('listener');
    }
329 330 331 332 333 334 335

    merged.addListener(listener);
    source1.notify();
    source2.notify();
    expect(log, <String>['listener', 'listener']);
    log.clear();

336
    merged.removeListener(listener);
337 338 339 340
    source1.notify();
    source2.notify();
    expect(log, isEmpty);
  });
341

342
  test('Cannot use a disposed ChangeNotifier except for remove listener', () {
343
    final TestNotifier source = TestNotifier();
344
    source.dispose();
345 346 347 348 349 350 351 352 353
    expect(() {
      source.addListener(() {});
    }, throwsFlutterError);
    expect(() {
      source.dispose();
    }, throwsFlutterError);
    expect(() {
      source.notify();
    }, throwsFlutterError);
354
  });
Adam Barth's avatar
Adam Barth committed
355

356 357 358 359 360 361 362 363 364 365 366 367
  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);
  });

368 369 370 371 372 373 374 375 376 377 378 379 380 381
  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);
  });

Adam Barth's avatar
Adam Barth committed
382
  test('Value notifier', () {
383
    final ValueNotifier<double> notifier = ValueNotifier<double>(2.0);
Adam Barth's avatar
Adam Barth committed
384 385

    final List<double> log = <double>[];
386 387 388
    void listener() {
      log.add(notifier.value);
    }
Adam Barth's avatar
Adam Barth committed
389 390 391 392

    notifier.addListener(listener);
    notifier.value = 3.0;

393
    expect(log, equals(<double>[3.0]));
Adam Barth's avatar
Adam Barth committed
394 395 396 397 398
    log.clear();

    notifier.value = 3.0;
    expect(log, isEmpty);
  });
399 400

  test('Listenable.merge toString', () {
401 402
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
403

404
    Listenable listenableUnderTest = Listenable.merge(<Listenable>[]);
405 406
    expect(listenableUnderTest.toString(), 'Listenable.merge([])');

407
    listenableUnderTest = Listenable.merge(<Listenable?>[null]);
408 409
    expect(listenableUnderTest.toString(), 'Listenable.merge([null])');

410
    listenableUnderTest = Listenable.merge(<Listenable>[source1]);
411
    expect(
412
      listenableUnderTest.toString(),
413 414 415
      "Listenable.merge([Instance of 'TestNotifier'])",
    );

416
    listenableUnderTest = Listenable.merge(<Listenable>[source1, source2]);
417
    expect(
418
      listenableUnderTest.toString(),
419 420 421
      "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])",
    );

422
    listenableUnderTest = Listenable.merge(<Listenable?>[null, source2]);
423
    expect(
424
      listenableUnderTest.toString(),
425 426 427
      "Listenable.merge([null, Instance of 'TestNotifier'])",
    );
  });
428

429 430 431 432 433
  test('Listenable.merge does not leak', () {
    // Regression test for https://github.com/flutter/flutter/issues/25163.

    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
434
    void fakeListener() {}
435

436 437
    final Listenable listenableUnderTest =
        Listenable.merge(<Listenable>[source1, source2]);
438 439 440 441 442 443 444 445 446 447 448
    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);
  });

449
  test('hasListeners', () {
450
    final HasListenersTester<bool> notifier = HasListenersTester<bool>(true);
451
    expect(notifier.testHasListeners, isFalse);
452 453
    void test1() {}
    void test2() {}
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
    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);
  });

472 473 474 475 476 477 478 479 480 481 482 483 484
  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);
  });
485 486 487 488

  test('Throws FlutterError when disposed and called', () {
    final TestNotifier testNotifier = TestNotifier();
    testNotifier.dispose();
489
    FlutterError? error;
490 491 492 493 494 495
    try {
      testNotifier.dispose();
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
496
    expect(error, isFlutterError);
497
    expect(
498
      error!.toStringDeep(),
499 500 501 502 503
      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',
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526
      ),
    );
  });

  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',
527 528
      ),
    );
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
  });

  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();
554 555
  });

556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
  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,
591
      11,
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
    ];
    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>[]);
  });
613
}