blob: 8b07d4bb63a8de93ea479e894f668823bf4e5a36 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:observe/observe.dart';
import 'package:observe/src/dirty_check.dart' as dirty_check;
import 'package:unittest/unittest.dart';
import 'observe_test_utils.dart';
// Note: this ensures we run the dartanalyzer on the @observe package.
// @static-clean
const _VALUE = const Symbol('value');
main() {
// Note: to test the basic Observable system, we use ObservableBox due to its
// simplicity. We also test a variant that is based on dirty-checking.
observeTest('no observers at the start', () {
expect(dirty_check.allObservablesCount, 0);
});
group('WatcherModel', () { observeTests(watch: true); });
group('ObservableBox', () { observeTests(); });
group('dirtyCheck loops can be debugged', () {
var messages;
var subscription;
setUp(() {
messages = [];
subscription = Logger.root.onRecord.listen((record) {
messages.add(record.message);
});
});
tearDown(() {
subscription.cancel();
});
test('logs debug information', () {
var maxNumIterations = dirty_check.MAX_DIRTY_CHECK_CYCLES;
var x = new WatcherModel(0);
var sub = x.changes.listen(expectAsync1((_) { x.value++; },
count: maxNumIterations));
x.value = 1;
Observable.dirtyCheck();
expect(x.value, maxNumIterations + 1);
expect(messages.length, 2);
expect(messages[0], contains('Possible loop'));
expect(messages[1], contains('index 0'));
expect(messages[1], contains('object: $x'));
sub.cancel();
});
});
}
observeTests({bool watch: false}) {
final createModel = watch ? (x) => new WatcherModel(x)
: (x) => new ObservableBox(x);
// Track the subscriptions so we can clean them up in tearDown.
List subs;
int initialObservers;
setUp(() {
initialObservers = dirty_check.allObservablesCount;
subs = [];
if (watch) runAsync(Observable.dirtyCheck);
});
tearDown(() {
for (var sub in subs) sub.cancel();
performMicrotaskCheckpoint();
expect(dirty_check.allObservablesCount, initialObservers,
reason: 'Observable object leaked');
});
observeTest('no observers', () {
var t = createModel(123);
expect(t.value, 123);
t.value = 42;
expect(t.value, 42);
expect(t.hasObservers, false);
});
observeTest('listen adds an observer', () {
var t = createModel(123);
expect(t.hasObservers, false);
subs.add(t.changes.listen((n) {}));
expect(t.hasObservers, true);
});
observeTest('changes delived async', () {
var t = createModel(123);
int called = 0;
subs.add(t.changes.listen(expectAsync1((records) {
called++;
expectChanges(records, _changedValue(watch ? 1 : 2));
})));
t.value = 41;
t.value = 42;
expect(called, 0);
});
observeTest('cause changes in handler', () {
var t = createModel(123);
int called = 0;
subs.add(t.changes.listen(expectAsync1((records) {
called++;
expectChanges(records, _changedValue(1));
if (called == 1) {
// Cause another change
t.value = 777;
}
}, count: 2)));
t.value = 42;
});
observeTest('multiple observers', () {
var t = createModel(123);
verifyRecords(records) {
expectChanges(records, _changedValue(watch ? 1 : 2));
};
subs.add(t.changes.listen(expectAsync1(verifyRecords)));
subs.add(t.changes.listen(expectAsync1(verifyRecords)));
t.value = 41;
t.value = 42;
});
observeTest('performMicrotaskCheckpoint', () {
var t = createModel(123);
var records = [];
subs.add(t.changes.listen((r) { records.addAll(r); }));
t.value = 41;
t.value = 42;
expectChanges(records, [], reason: 'changes delived async');
performMicrotaskCheckpoint();
expectChanges(records, _changedValue(watch ? 1 : 2));
records.clear();
t.value = 777;
expectChanges(records, [], reason: 'changes delived async');
performMicrotaskCheckpoint();
expectChanges(records, _changedValue(1));
// Has no effect if there are no changes
performMicrotaskCheckpoint();
expectChanges(records, _changedValue(1));
});
observeTest('cancel listening', () {
var t = createModel(123);
var sub;
sub = t.changes.listen(expectAsync1((records) {
expectChanges(records, _changedValue(1));
sub.cancel();
t.value = 777;
runAsync(Observable.dirtyCheck);
}));
t.value = 42;
});
observeTest('cancel and reobserve', () {
var t = createModel(123);
var sub;
sub = t.changes.listen(expectAsync1((records) {
expectChanges(records, _changedValue(1));
sub.cancel();
runAsync(expectAsync0(() {
subs.add(t.changes.listen(expectAsync1((records) {
expectChanges(records, _changedValue(1));
})));
t.value = 777;
runAsync(Observable.dirtyCheck);
}));
}));
t.value = 42;
});
observeTest('cannot modify changes list', () {
var t = createModel(123);
var records = null;
subs.add(t.changes.listen((r) { records = r; }));
t.value = 42;
performMicrotaskCheckpoint();
expectChanges(records, _changedValue(1));
// Verify that mutation operations on the list fail:
expect(() {
records[0] = new PropertyChangeRecord(_VALUE);
}, throwsUnsupportedError);
expect(() { records.clear(); }, throwsUnsupportedError);
expect(() { records.length = 0; }, throwsUnsupportedError);
});
observeTest('notifyChange', () {
var t = createModel(123);
var records = [];
subs.add(t.changes.listen((r) { records.addAll(r); }));
t.notifyChange(new PropertyChangeRecord(_VALUE));
performMicrotaskCheckpoint();
expectChanges(records, _changedValue(1));
expect(t.value, 123, reason: 'value did not actually change.');
});
observeTest('notifyPropertyChange', () {
var t = createModel(123);
var records = null;
subs.add(t.changes.listen((r) { records = r; }));
expect(t.notifyPropertyChange(_VALUE, t.value, 42), 42,
reason: 'notifyPropertyChange returns newValue');
performMicrotaskCheckpoint();
expectChanges(records, _changedValue(1));
expect(t.value, 123, reason: 'value did not actually change.');
});
}
_changedValue(len) => new List.filled(len, new PropertyChangeRecord(_VALUE));
// A test model based on dirty checking.
class WatcherModel<T> extends ObservableBase {
@observable T value;
WatcherModel([T initialValue]) : value = initialValue;
String toString() => '#<$runtimeType value: $value>';
}