diff --git a/mobx/CHANGELOG.md b/mobx/CHANGELOG.md index 5ed353fba..d3092af69 100644 --- a/mobx/CHANGELOG.md +++ b/mobx/CHANGELOG.md @@ -1,11 +1,15 @@ +## 2.1.4 + +- feat: add ObservableQueue by [@amondnet](https://github.com/amondnet) in [#892](https://github.com/mobxjs/mobx.dart/pull/892) + ## 2.1.2 - 2.1.3 -- Fix tests in dart 2.19 - [@amondnet](https://github.com/amondnet) +- Fix tests in dart 2.19 by [@amondnet](https://github.com/amondnet) in [#879](https://github.com/mobxjs/mobx.dart/pull/879) - Dart formatting fixes ## 2.1.1 -- Allow a custom equals parameter for ObservableStream - [@amondnet](https://github.com/amondnet) +- Allow a custom equals parameter for ObservableStream by [@amondnet](https://github.com/amondnet) in [#771](https://github.com/mobxjs/mobx.dart/pull/771) ## 2.1.0 diff --git a/mobx/lib/src/api/observable_collections.dart b/mobx/lib/src/api/observable_collections.dart index 6a6361634..e72d8132a 100644 --- a/mobx/lib/src/api/observable_collections.dart +++ b/mobx/lib/src/api/observable_collections.dart @@ -7,3 +7,4 @@ import 'package:mobx/mobx.dart'; part 'observable_collections/observable_list.dart'; part 'observable_collections/observable_map.dart'; part 'observable_collections/observable_set.dart'; +part 'observable_collections/observable_queue.dart'; diff --git a/mobx/lib/src/api/observable_collections/observable_queue.dart b/mobx/lib/src/api/observable_collections/observable_queue.dart new file mode 100644 index 000000000..4c2752e5d --- /dev/null +++ b/mobx/lib/src/api/observable_collections/observable_queue.dart @@ -0,0 +1,433 @@ +part of '../observable_collections.dart'; + +Atom _observableQueueAtom(ReactiveContext? context, String? name) { + final ctx = context ?? mainContext; + return Atom(name: name ?? ctx.nameFor('ObservableQueue<$T>'), context: ctx); +} + +/// The ObservableQueue tracks the various read-methods (eg: [List.first], [List.last]) and +/// write-methods (eg: [Queue.add], [Queue.addFirst], [Queue.addLast]) making it easier to use it inside reactions. +/// +/// As the name suggests, this is the Observable-counterpart to the standard Dart `Queue`. +/// +/// ```dart +/// final queue = ObservableQueue.of([1]); +/// +/// autorun((_) { +/// print(queue.first); +/// }) // prints 1 +/// +/// queue.addFirst(100); // autorun prints 100 +/// ``` +class ObservableQueue extends ListQueue + with + // ignore: prefer_mixin + Queue + implements + Listenable> { + ObservableQueue({ReactiveContext? context, String? name}) + : this._wrap(context, _observableQueueAtom(context, name), Queue()); + + ObservableQueue.of(Iterable elements, + {ReactiveContext? context, String? name}) + : this._wrap(context, _observableQueueAtom(context, name), + Queue.of(elements)); + + ObservableQueue._wrap(ReactiveContext? context, this._atom, this._queue) + : _context = context ?? mainContext; + + final ReactiveContext _context; + final Atom _atom; + final Queue _queue; + + Listeners>? _listenersField; + + Listeners> get _listeners => + _listenersField ??= Listeners(_context); + + /// The name used to identify for debugging purposes + String get name => _atom.name; + + @override + int get length { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.length; + } + + void _notifyAdd(T value, [int index = 0]) { + _atom.reportChanged(); + _reportAdd(value, index); + } + + void _reportAdd(T value, [int index = 0]) { + _listeners.notifyListeners(QueueChange(queue: this, elementChanges: [ + QueueElementChange(index: index, oldValue: null, newValue: value) + ])); + } + + void _notifyRemove(T value, [int index = 0]) { + _atom.reportChanged(); + _reportRemove(value, index); + } + + void _reportRemove(T? value, int index) { + _listeners.notifyListeners(QueueChange(queue: this, elementChanges: [ + QueueElementChange( + index: index, + type: OperationType.remove, + newValue: null, + oldValue: value) + ])); + } + + @override + void add(T value) { + _context.conditionallyRunInAction(() { + final index = _queue.length; + _queue.add(value); + _notifyAdd(value, index); + }, _atom); + } + + @override + void addAll(Iterable iterable) { + _context.conditionallyRunInAction(() { + if (iterable.isNotEmpty) { + final index = _queue.length; + _queue.addAll(iterable); + + _notifyRangeUpdate(index, iterable.toList(growable: false), null); + } + }, _atom); + } + + @override + Iterator get iterator => ObservableIterator(_atom, _queue.iterator); + + @override + T lastWhere(bool Function(T element) test, {T Function()? orElse}) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.lastWhere(test, orElse: orElse); + } + + @override + T get single { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.single; + } + + @override + List toList({bool growable = true}) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.toList(growable: growable); + } + + @override + void clear() { + _context.conditionallyRunInAction(() { + if (_queue.isNotEmpty) { + final oldItems = _queue.toList(growable: false); + _queue.clear(); + _notifyRangeUpdate(0, null, oldItems); + } + }, _atom); + } + + @override + bool remove(Object? value) { + var didRemove = false; + + _context.conditionallyRunInAction(() { + for (var i = _queue.length - 1; i >= 0; --i) { + final element = _queue.elementAt(i); + if (element == value) { + _queue.remove(value as T); + _notifyRemove(value, i); + didRemove = true; + } + } + }, _atom); + + return didRemove; + } + + @override + T removeLast() { + late T value; + + _context.conditionallyRunInAction(() { + value = _queue.removeLast(); + // Index is _queue.length as it points to the index before the last element is removed + _notifyRemove(value, _queue.length); + }, _atom); + + return value; + } + + @override + void removeWhere(bool Function(T element) test) { + _context.conditionallyRunInAction(() { + final removedElements = Queue>(); + for (var i = _queue.length - 1; i >= 0; --i) { + final element = _queue.elementAt(i); + if (test(element)) { + removedElements.addFirst(QueueElementChange( + index: i, oldValue: element, type: OperationType.remove)); + } + } + _queue.removeWhere(test); + if (removedElements.isNotEmpty) { + _notifyElementsUpdate(removedElements.toList(growable: false)); + } + }, _atom); + } + + @override + void retainWhere(bool Function(T element) test) { + _context.conditionallyRunInAction(() { + final removedElements = Queue>(); + for (var i = _queue.length - 1; i >= 0; --i) { + final element = _queue.elementAt(i); + if (!test(element)) { + removedElements.addFirst(QueueElementChange( + index: i, oldValue: element, type: OperationType.remove)); + } + } + _queue.retainWhere(test); + if (removedElements.isNotEmpty) { + _notifyElementsUpdate(removedElements.toList(growable: false)); + } + }, _atom); + } + + /// Attaches a listener to changes happening in the [ObservableQueue]. You have + /// the option to be notified immediately ([fireImmediately]) or wait for until the first change. + @override + Dispose observe(Listener> listener, + {bool fireImmediately = false}) { + final dispose = _listeners.add(listener); + if (fireImmediately == true) { + _queue.forEach(_reportAdd); + } + return dispose; + } + + @override + void addFirst(T value) { + _context.conditionallyRunInAction(() { + _queue.addFirst(value); + _notifyAdd(value, 0); + }, _atom); + } + + @override + void addLast(T value) { + _context.conditionallyRunInAction(() { + _queue.addLast(value); + _notifyAdd(value, _queue.length); + }, _atom); + } + + @override + bool contains(Object? element) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.contains(element); + } + + @override + T elementAt(int index) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + + return _queue.elementAt(index); + } + + @override + T get first { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.first; + } + + @override + T firstWhere(bool Function(T element) test, {T Function()? orElse}) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.firstWhere(test, orElse: orElse); + } + + @override + bool get isEmpty { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.isEmpty; + } + + @override + bool get isNotEmpty { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.isNotEmpty; + } + + @override + T get last { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.last; + } + + @override + T removeFirst() { + late T value; + + _context.conditionallyRunInAction(() { + value = _queue.removeFirst(); + _notifyRemove(value, 0); + }, _atom); + + return value; + } + + @override + T singleWhere(bool Function(T element) test, {T Function()? orElse}) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.singleWhere(test, orElse: orElse); + } + + @override + Set toSet() { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.toSet(); + } + + void _notifyElementsUpdate(final List> elementChanges) { + _atom.reportChanged(); + + final change = QueueChange(queue: this, elementChanges: elementChanges); + + _listeners.notifyListeners(change); + } + + void _notifyRangeUpdate(int index, List? newValues, List? oldValues) { + _atom.reportChanged(); + + final change = + QueueChange(queue: this, rangeChanges: >[ + QueueRangeChange(index: index, newValues: newValues, oldValues: oldValues) + ]); + + _listeners.notifyListeners(change); + } + + @override + bool any(bool Function(T element) test) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.any(test); + } + + @override + Queue cast() => + ObservableQueue._wrap(_context, _atom, _queue.cast()); + + @override + bool every(bool Function(T element) test) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.every(test); + } + + @override + void forEach(void Function(T element) f) { + _context.enforceReadPolicy(_atom); + + _atom.reportObserved(); + return _queue.forEach(f); + } +} + +typedef QueueChangeListener = void Function( + QueueChange); + +/// Stores the change in the value of an element with specific [index]. +/// +/// The value [OperationType.add] of [type] means inserting the element with value +/// [newValue] in the queue. +/// +/// The value [OperationType.remove] of [type] means the element with value [oldValue] +/// was removed from the queue. +class QueueElementChange { + QueueElementChange( + {required this.index, + this.type = OperationType.add, + this.newValue, + this.oldValue}); + + final int index; + final OperationType type; + final T? newValue; + final T? oldValue; +} + +/// Stores the change of values in a range of [ObservableQueue] started with specific [index]. +/// +/// The values of elements in the range were changed if [oldValues] and [newValues] are not null +/// and have the same length. +/// +/// The elements were added to the queue if [newValues] is set and not empty, and [oldValues] is +/// null. +/// +/// The elements were removed from the queue if [oldValues] is set and not empty, and [newValues] +/// is null. +class QueueRangeChange { + QueueRangeChange({required this.index, this.newValues, this.oldValues}); + + final int index; + final List? newValues; + final List? oldValues; +} + +enum ElementPosition { first, last } + +/// Stores the change related information when items was modified, added or removed from [list]. +/// +/// The [elementChanges] object stores change mappings for the indexes of changed elements. +/// The [rangeChanges] object stores mappings of the changed ranges to the indexes of the first +/// elements of this ranges. +/// These two objects cannot overlap (cannot contain the same indexes of changed elements), in +/// most cases only one of them will be defined. +class QueueChange { + QueueChange({required this.queue, this.elementChanges, this.rangeChanges}); + + final ObservableQueue queue; + final List>? elementChanges; + final List>? rangeChanges; +} + +/// Used during testing for wrapping a regular `Queue` as an `ObservableQueue` +@visibleForTesting +ObservableQueue wrapInObservableQueue(Atom atom, Queue queue) => + ObservableQueue._wrap(mainContext, atom, queue); diff --git a/mobx/lib/version.dart b/mobx/lib/version.dart index 34cc06f1b..e39cab4f5 100644 --- a/mobx/lib/version.dart +++ b/mobx/lib/version.dart @@ -1,4 +1,4 @@ // Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!! /// The current version as per `pubspec.yaml`. -const version = '2.1.3'; +const version = '2.1.4'; diff --git a/mobx/pubspec.yaml b/mobx/pubspec.yaml index fb1ad30cf..013672e6b 100644 --- a/mobx/pubspec.yaml +++ b/mobx/pubspec.yaml @@ -1,5 +1,5 @@ name: mobx -version: 2.1.3 +version: 2.1.4 description: "MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter apps." homepage: https://github.com/mobxjs/mobx.dart diff --git a/mobx/test/all_tests.dart b/mobx/test/all_tests.dart index fb1c6705e..c32f9865b 100644 --- a/mobx/test/all_tests.dart +++ b/mobx/test/all_tests.dart @@ -23,6 +23,7 @@ import 'observable_future_test.dart' as observable_future_test; import 'observable_list_test.dart' as observable_list_test; import 'observable_map_test.dart' as observable_map_test; import 'observable_set_test.dart' as observable_set_test; +import 'observable_queue_test.dart' as observable_queue_test; import 'observable_stream_test.dart' as observable_stream_test; import 'observable_test.dart' as observable_test; import 'observable_value_test.dart' as observable_value_test; @@ -41,6 +42,7 @@ void main() { observable_list_test.main(); observable_map_test.main(); observable_set_test.main(); + observable_queue_test.main(); observable_future_test.main(); observable_stream_test.main(); diff --git a/mobx/test/observable_queue_test.dart b/mobx/test/observable_queue_test.dart new file mode 100644 index 000000000..3e5ebaf7f --- /dev/null +++ b/mobx/test/observable_queue_test.dart @@ -0,0 +1,510 @@ +import 'dart:collection'; + +import 'package:mobx/mobx.dart'; +import 'package:mobx/src/api/observable_collections.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import 'shared_mocks.dart'; +import 'util.dart'; + +// ignore_for_file: unnecessary_lambdas + +void main() { + testSetup(); + + group('ObservableQueue', () { + test('generates a name if not given', () { + final queue = ObservableQueue.of([]); + expect(queue.name, matches(RegExp(r'ObservableQueue\<.*\>@'))); + }); + + test('uses the name if given', () { + final queue = ObservableQueue.of([], name: 'test'); + expect(queue.name, equals('test')); + }); + + test('basics work', () { + final queue = ObservableQueue(); + var count = -1; + + expect(queue.name, startsWith('ObservableQueue@')); + + final d = autorun((_) { + count = queue.length; + }); + + expect(count, equals(0)); + + queue.add(20); + expect(count, equals(1)); + d(); + }); + + test('cast returns a live view to queue', () { + final queue = ObservableQueue.of([0, 1, 2, 3, 4, 5, 6]); + final casted = queue.cast(); + + var count = 0; + autorun((_) { + // ignore:unnecessary_statements + casted.first; + count++; + }); + expect(count, equals(1)); + + queue.addFirst(99); + expect(casted.first, equals(99)); + expect(count, equals(2)); + }); + + test('Autorun should execute when items are added to an empty queue', () { + final queue = ObservableQueue(); + + var count = 0; + autorun((_) { + // ignore: unused_local_variable + for (final x in queue) { + count++; + } + }); + expect(count, equals(0)); + + queue.add(0); + expect(count, equals(1)); + }); + + test('observe basics work', () { + final queue = ObservableQueue.of([0]); + + var count = 0; + + queue + ..observe((change) { + count++; + }) + ..add(1); + + expect(count, equals(1)); + }); + + test('observe with fireImmediately works', () { + final queue = ObservableQueue.of([0]); + + var count = 0; + + queue + ..observe((change) { + count++; + }, fireImmediately: true) + ..add(1); + + // 1 + 1: fireImmediately + add + expect(count, equals(2)); + }); + + test('observe add item works', () { + final queue = ObservableQueue.of([0]); + + var index = -1; + var addedValues = []; + List? removedValues = []; + + queue + ..observe((change) { + if (change.rangeChanges != null) { + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges!.first.newValues!; + removedValues = change.rangeChanges!.first.oldValues!; + } else if (change.elementChanges != null) { + expect(change.elementChanges!.length, equals(1)); + index = change.elementChanges!.first.index; + expect( + change.elementChanges!.first.type, equals(OperationType.add)); + addedValues = [change.elementChanges!.first.newValue!]; + removedValues = null; + } + }) + ..add(1); + + expect(index, equals(1)); + expect(addedValues, equals([1])); + expect(removedValues, equals(null)); + }); + + test('observe addAll items works', () { + final queue = ObservableQueue.of([0]); + + var index = -1; + List? addedValues; + List? removedValues; + + queue + ..observe((change) { + expect(change.rangeChanges, isNotNull); + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges?.first.newValues; + removedValues = change.rangeChanges?.first.oldValues; + }) + ..addAll([1, 2]); + + expect(index, equals(1)); + expect(addedValues, equals([1, 2])); + expect(removedValues, equals(null)); + }); + + test('observe add first works', () { + final queue = ObservableQueue.of([0]); + + var index = -1; + var addedValues = []; + List? removedValues = []; + + queue + ..observe((change) { + if (change.rangeChanges != null) { + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges!.first.newValues!; + removedValues = change.rangeChanges!.first.oldValues!; + } else if (change.elementChanges != null) { + expect(change.elementChanges!.length, equals(1)); + index = change.elementChanges!.first.index; + expect( + change.elementChanges!.first.type, equals(OperationType.add)); + addedValues = [change.elementChanges!.first.newValue!]; + removedValues = null; + } + }) + ..addFirst(1); + + expect(index, equals(0)); + expect(addedValues, equals([1])); + expect(removedValues, equals(null)); + }); + + test('observe clear works', () { + final queue = ObservableQueue.of([0, 1, 2]); + + var index = -1; + List? addedValues; + List? removedValues; + + queue + ..observe((change) { + expect(change.rangeChanges, isNotNull); + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges?.first.newValues; + removedValues = change.rangeChanges?.first.oldValues; + }) + ..clear(); + + expect(index, equals(0)); + expect(addedValues, equals(null)); + expect(removedValues, equals([0, 1, 2])); + }); + + test('observe remove works', () { + final queue = ObservableQueue.of([0, 1, null]); + + var index = -1; + List? addedValues = []; + List removedValues = []; + + queue.observe((change) { + if (change.rangeChanges != null) { + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges!.first.newValues!; + removedValues = change.rangeChanges!.first.oldValues!; + } else if (change.elementChanges != null) { + expect(change.elementChanges!.length, equals(1)); + index = change.elementChanges!.first.index; + expect( + change.elementChanges!.first.type, equals(OperationType.remove)); + addedValues = null; + removedValues = [change.elementChanges!.first.oldValue]; + } + }); + + expect(queue.remove(-1), equals(false)); + + expect(queue.remove(1), equals(true)); + expect(index, equals(1)); + expect(addedValues, equals(null)); + expect(removedValues, equals([1])); + + expect(queue.remove(null), equals(true)); + expect(index, equals(1)); + expect(addedValues, equals(null)); + expect(removedValues, equals([null])); + }); + + test('observe removeLast works', () { + final queue = ObservableQueue.of([0, 1, 2, 3]); + + var index = -1; + List? addedValues = []; + var removedValues = []; + + queue + ..observe((change) { + if (change.rangeChanges != null) { + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges!.first.newValues!; + removedValues = change.rangeChanges!.first.oldValues!; + } else if (change.elementChanges != null) { + expect(change.elementChanges!.length, equals(1)); + index = change.elementChanges!.first.index; + expect(change.elementChanges!.first.type, + equals(OperationType.remove)); + addedValues = null; + removedValues = [change.elementChanges!.first.oldValue!]; + } + }) + ..removeLast(); + + expect(index, equals(3)); + expect(addedValues, equals(null)); + expect(removedValues, equals([3])); + }); + + test('observe removeFirst works', () { + final queue = ObservableQueue.of([0, -1, -2, 3]); + + var index = -1; + List? addedValues; + List? removedValues; + + queue + ..observe((change) { + if (change.rangeChanges != null) { + expect(change.rangeChanges!.length, equals(1)); + index = change.rangeChanges!.first.index; + addedValues = change.rangeChanges!.first.newValues!; + removedValues = change.rangeChanges!.first.oldValues!; + } else if (change.elementChanges != null) { + expect(change.elementChanges!.length, equals(1)); + index = change.elementChanges!.first.index; + expect(change.elementChanges!.first.type, + equals(OperationType.remove)); + addedValues = null; + removedValues = [change.elementChanges!.first.oldValue!]; + } + }) + ..removeFirst(); + + expect(index, equals(0)); + expect(addedValues, equals(null)); + expect(removedValues, equals([0])); + }); + + test('observe removeWhere works', () { + final queue = ObservableQueue.of([0, -1, 2, -3]); + + final indexes = []; + final removedValues = []; + + queue + ..observe((change) { + expect(change.elementChanges, isNotNull); + for (var elementChange in change.elementChanges!) { + expect(elementChange.type, equals(OperationType.remove)); + indexes.add(elementChange.index); + removedValues.add(elementChange.oldValue!); + } + }) + ..removeWhere((element) => element < 0); + + expect(indexes, equals([1, 3])); + expect(removedValues, equals([-1, -3])); + }); + + test('observe retainWhere works', () { + final queue = ObservableQueue.of([0, -1, 2, -3]); + + final indexes = []; + final removedValues = []; + + queue + ..observe((change) { + expect(change.elementChanges, isNotNull); + for (var elementChange in change.elementChanges!) { + expect(elementChange.type, equals(OperationType.remove)); + indexes.add(elementChange.index); + removedValues.add(elementChange.oldValue!); + } + }) + ..retainWhere((element) => element < 0); + + expect(indexes, equals([0, 2])); + expect(removedValues, equals([0, 2])); + }); + + group('fires reportObserved() for read-methods', () { + )>{ + 'isEmpty': (_) => _.isEmpty, + 'isNotEmpty': (_) => _.isNotEmpty, + 'single': (_) => _ignoreException(() => _.single), + 'first': (_) => _ignoreException(() => _.first), + 'last': (_) => _ignoreException(() => _.last), + 'toSet': (_) => _.toSet(), + 'toList': (_) => _.toList(), + 'join': (_) => _.join(), + 'fold': (_) => _.fold(0, (sum, item) => sum), + 'elementAt': (_) => _ignoreException(() => _.elementAt(0)), + 'singleWhere': (_) => _ignoreException( + () => _.singleWhere((_) => _ == 20, orElse: () => 0)), + + // ignore: avoid_function_literals_in_foreach_calls + 'forEach': (_) => _.forEach((a) {}), + + 'contains': (_) => _.contains(null), + 'lastWhere': (_) => _.lastWhere((_) => true, orElse: () => 0), + 'firstWhere': (_) => _.firstWhere((_) => true, orElse: () => 0), + 'every': (_) => _.every((_) => true), + 'any': (_) => _.any((_) => true), + }.forEach(_templateReadTest); + }); + }); + + group( + 'fires reportObserved() for iterable transformation methods only when iterating', + () { + )>{ + 'cast': (m) => m.cast(), + 'expand': (m) => m.expand((i) => [3, 4]), + 'followedBy': (m) => m.followedBy([5, 6]), + 'map': (m) => m.map((i) => i + 1), + 'skip': (m) => m.skip(1), + 'skipWhile': (m) => m.skipWhile((i) => i < 2), + 'take': (m) => m.take(2), + 'takeWhile': (m) => m.takeWhile((i) => i < 3), + 'where': (m) => m.where((i) => i > 2), + 'whereType': (m) => m.whereType(), + }.forEach(runIterableTest); + }); + + group('fires reportChanged() for write-methods', () { + )>{ + 'add': (_) { + _.add(100); + return true; + }, + 'addFirst': (_) { + _.addFirst(100); + return true; + }, + 'addLast': (_) { + _.addLast(100); + return true; + }, + 'addAll': (_) { + _.addAll([100]); + return true; + }, + 'clear': (_) { + _.clear(); + return true; + }, + 'removeLast': (_) { + _.removeLast(); + return true; + }, + 'remove': (_) { + _.remove(0); + return true; + }, + 'removeWhere': (_) { + _.removeWhere((_) => true); + return true; + }, + 'retainWhere': (_) { + _.retainWhere((_) => false); + return true; + }, + '!addAll': (_) { + _.addAll([]); + return false; + }, + '!remove': (_) { + _.remove(-1); + return false; + }, + '!removeWhere': (_) { + _.removeWhere((_) => false); + return false; + }, + '!retainWhere': (_) { + _.retainWhere((_) => true); + return false; + }, + }.forEach(_templateWriteTest); + }); +} + +dynamic _ignoreException(Function fn) { + try { + return fn(); + } on Object catch (_) { + // Catching on Object since it takes care of both Error and Exception + // Ignore + return null; + } +} + +void _templateReadTest( + String description, void Function(ObservableQueue) fn) { + test(description, () { + final atom = MockAtom(); + final queue = wrapInObservableQueue(atom, Queue.of([0, 1, 2, 3])); + + verifyNever(() => atom.reportChanged()); + verifyNever(() => atom.reportObserved()); + + fn(queue); + + verify(() => atom.reportObserved()); + verifyNever(() => atom.reportChanged()); + }); +} + +void _templateWriteTest( + String description, bool Function(ObservableQueue) fn) { + test(description, () { + final atom = MockAtom(); + final queue = wrapInObservableQueue(atom, Queue.of([0, 1, 2, 3])); + + verifyNever(() => atom.reportChanged()); + verifyNever(() => atom.reportObserved()); + + // fire the method caused or not the reportChanged() to be invoked. + fn(queue) + ? verify(() => atom.reportChanged()) + : verifyNever(() => atom.reportChanged()); + }); +} + +void runIterableTest( + String description, Iterable Function(ObservableQueue) body) { + test(description, () { + final atom = MockAtom(); + final map = wrapInObservableQueue(atom, Queue.of([1, 2, 3, 4])); + + verifyNever(() => atom.reportChanged()); + verifyNever(() => atom.reportObserved()); + + final iter = body(map); + verifyNever(() => atom.reportChanged()); + verifyNever(() => atom.reportObserved()); + + void noOp(_) {} + iter.forEach(noOp); + verify(() => atom.reportObserved()); + verifyNever(() => atom.reportChanged()); + }); +}