change_notifier_test.dart 13.8 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 17 18 19 20 21 22
class HasListenersTester<T> extends ValueNotifier<T> {
  HasListenersTester(T value) : super(value);
  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 52 53
void main() {
  testWidgets('ChangeNotifier', (WidgetTester tester) async {
    final List<String> log = <String>[];
54 55 56 57 58 59 60 61 62 63 64 65
    void listener() {
      log.add('listener');
    }

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

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

66
    void badListener() {
67
      log.add('badListener');
68
      throw ArgumentError();
69
    }
70

71
    final TestNotifier test = TestNotifier();
72 73 74 75

    test.addListener(listener);
    test.addListener(listener);
    test.notify();
76
    expect(log, <String>['listener', 'listener']);
77 78 79 80
    log.clear();

    test.removeListener(listener);
    test.notify();
81
    expect(log, <String>['listener']);
82 83 84 85
    log.clear();

    test.removeListener(listener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
86
    expect(log, <String>[]);
87 88 89 90
    log.clear();

    test.removeListener(listener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
91
    expect(log, <String>[]);
92 93 94 95
    log.clear();

    test.addListener(listener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
96
    expect(log, <String>['listener']);
97 98 99 100
    log.clear();

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

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

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

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

    test.addListener(badListener);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
121
    expect(log, <String>['listener', 'listener2', 'listener1', 'badListener']);
122
    expect(tester.takeException(), isArgumentError);
123 124 125 126 127 128 129 130
    log.clear();

    test.addListener(listener1);
    test.removeListener(listener);
    test.removeListener(listener1);
    test.removeListener(listener2);
    test.addListener(listener2);
    test.notify();
131
    expect(log, <String>['badListener', 'listener1', 'listener2']);
132
    expect(tester.takeException(), isArgumentError);
133
    log.clear();
Kate Lovett's avatar
Kate Lovett committed
134
  });
135

136
  test('ChangeNotifier with mutating listener', () {
137
    final TestNotifier test = TestNotifier();
138 139
    final List<String> log = <String>[];

140 141 142 143 144 145 146 147 148 149 150 151
    void listener1() {
      log.add('listener1');
    }

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

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

152
    void listener2() {
153 154 155 156
      log.add('listener2');
      test.removeListener(listener1);
      test.removeListener(listener3);
      test.addListener(listener4);
157
    }
158 159 160 161 162

    test.addListener(listener1);
    test.addListener(listener2);
    test.addListener(listener3);
    test.notify();
Ian Hickson's avatar
Ian Hickson committed
163
    expect(log, <String>['listener1', 'listener2']);
164 165 166
    log.clear();

    test.notify();
Ian Hickson's avatar
Ian Hickson committed
167
    expect(log, <String>['listener2', 'listener4']);
168 169 170
    log.clear();

    test.notify();
171
    expect(log, <String>['listener2', 'listener4', 'listener4']);
Ian Hickson's avatar
Ian Hickson committed
172 173 174
    log.clear();
  });

175
  test('During notifyListeners, a listener was added and removed immediately', () {
176 177 178
    final TestNotifier source = TestNotifier();
    final List<String> log = <String>[];

179 180 181 182 183 184 185 186
    void listener3() {
      log.add('listener3');
    }

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

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
    void listener1() {
      log.add('listener1');
      source.addListener(listener2);
      source.removeListener(listener2);
      source.addListener(listener3);
    }

    source.addListener(listener1);

    source.notify();

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

  test(
202 203 204 205 206 207 208 209 210 211
    '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);
      }
212

213 214 215
      void listener1() {
        log.add('listener1');
      }
216

217 218 219
      source.addListener(listener1);
      source.addListener(selfRemovingListener);
      source.addListener(listener1);
220

221
      source.notify();
222

223 224 225
      expect(log, <String>['listener1', 'selfRemovingListener', 'listener1']);
    },
  );
226

227
  test('If the first listener removes itself, notifyListeners still notify all listeners', () {
228 229 230 231 232 233 234
    final TestNotifier source = TestNotifier();
    final List<String> log = <String>[];

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

236 237 238 239 240 241 242 243 244 245 246 247
    void listener1() {
      log.add('listener1');
    }

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

    source.notifyListeners();

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

248
  test('Merging change notifiers', () {
249 250 251
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
    final TestNotifier source3 = TestNotifier();
Ian Hickson's avatar
Ian Hickson committed
252 253
    final List<String> log = <String>[];

254
    final Listenable merged = Listenable.merge(<Listenable>[source1, source2]);
255 256 257 258 259 260 261
    void listener1() {
      log.add('listener1');
    }

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

263
    merged.addListener(listener1);
Ian Hickson's avatar
Ian Hickson committed
264 265 266
    source1.notify();
    source2.notify();
    source3.notify();
267
    expect(log, <String>['listener1', 'listener1']);
Ian Hickson's avatar
Ian Hickson committed
268 269
    log.clear();

270
    merged.removeListener(listener1);
Ian Hickson's avatar
Ian Hickson committed
271 272 273 274
    source1.notify();
    source2.notify();
    source3.notify();
    expect(log, isEmpty);
275
    log.clear();
276 277 278 279 280 281 282 283 284 285

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

286
  test('Merging change notifiers ignores null', () {
287 288
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
289 290
    final List<String> log = <String>[];

291 292 293 294 295
    final Listenable merged =
        Listenable.merge(<Listenable?>[null, source1, null, source2, null]);
    void listener() {
      log.add('listener');
    }
296 297 298 299 300 301

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

304
  test('Can remove from merged notifier', () {
305 306
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
307 308
    final List<String> log = <String>[];

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

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

320
    merged.removeListener(listener);
321 322 323 324
    source1.notify();
    source2.notify();
    expect(log, isEmpty);
  });
325 326

  test('Cannot use a disposed ChangeNotifier', () {
327
    final TestNotifier source = TestNotifier();
328
    source.dispose();
329 330 331 332 333 334 335 336 337 338 339 340
    expect(() {
      source.addListener(() {});
    }, throwsFlutterError);
    expect(() {
      source.removeListener(() {});
    }, throwsFlutterError);
    expect(() {
      source.dispose();
    }, throwsFlutterError);
    expect(() {
      source.notify();
    }, throwsFlutterError);
341
  });
Adam Barth's avatar
Adam Barth committed
342 343

  test('Value notifier', () {
344
    final ValueNotifier<double> notifier = ValueNotifier<double>(2.0);
Adam Barth's avatar
Adam Barth committed
345 346

    final List<double> log = <double>[];
347 348 349
    void listener() {
      log.add(notifier.value);
    }
Adam Barth's avatar
Adam Barth committed
350 351 352 353

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

354
    expect(log, equals(<double>[3.0]));
Adam Barth's avatar
Adam Barth committed
355 356 357 358 359
    log.clear();

    notifier.value = 3.0;
    expect(log, isEmpty);
  });
360 361

  test('Listenable.merge toString', () {
362 363
    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
364

365
    Listenable listenableUnderTest = Listenable.merge(<Listenable>[]);
366 367
    expect(listenableUnderTest.toString(), 'Listenable.merge([])');

368
    listenableUnderTest = Listenable.merge(<Listenable?>[null]);
369 370
    expect(listenableUnderTest.toString(), 'Listenable.merge([null])');

371
    listenableUnderTest = Listenable.merge(<Listenable>[source1]);
372
    expect(
373
      listenableUnderTest.toString(),
374 375 376
      "Listenable.merge([Instance of 'TestNotifier'])",
    );

377
    listenableUnderTest = Listenable.merge(<Listenable>[source1, source2]);
378
    expect(
379
      listenableUnderTest.toString(),
380 381 382
      "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])",
    );

383
    listenableUnderTest = Listenable.merge(<Listenable?>[null, source2]);
384
    expect(
385
      listenableUnderTest.toString(),
386 387 388
      "Listenable.merge([null, Instance of 'TestNotifier'])",
    );
  });
389

390 391 392 393 394
  test('Listenable.merge does not leak', () {
    // Regression test for https://github.com/flutter/flutter/issues/25163.

    final TestNotifier source1 = TestNotifier();
    final TestNotifier source2 = TestNotifier();
395
    void fakeListener() {}
396

397 398
    final Listenable listenableUnderTest =
        Listenable.merge(<Listenable>[source1, source2]);
399 400 401 402 403 404 405 406 407 408 409
    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);
  });

410
  test('hasListeners', () {
411
    final HasListenersTester<bool> notifier = HasListenersTester<bool>(true);
412
    expect(notifier.testHasListeners, isFalse);
413 414
    void test1() {}
    void test2() {}
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
    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);
  });

433 434 435 436 437 438 439 440 441 442 443 444 445
  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);
  });
446 447 448 449

  test('Throws FlutterError when disposed and called', () {
    final TestNotifier testNotifier = TestNotifier();
    testNotifier.dispose();
450
    FlutterError? error;
451 452 453 454 455 456
    try {
      testNotifier.dispose();
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
457
    expect(error, isFlutterError);
458
    expect(
459
      error!.toStringDeep(),
460 461 462 463 464 465 466
      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',
      ),
    );
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
  });

  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();
492 493
  });

494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
  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,
529
      11,
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
    ];
    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>[]);
  });
551
}