Skip to content

Commit 51e8665

Browse files
committed
Add a signals app tests
1 parent c16847c commit 51e8665

File tree

7 files changed

+317
-58
lines changed

7 files changed

+317
-58
lines changed

signals/lib/app.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class SignalsApp extends StatelessWidget {
1717
@override
1818
Widget build(BuildContext context) {
1919
return Provider<TodoListController>(
20-
create: (_) => TodoListController(todosRepository: repository)..init(),
20+
create: (_) => TodoListController(repository: repository)..init(),
2121
dispose: (_, controller) => controller.dispose(),
2222
child: MaterialApp(
2323
theme: ArchSampleTheme.theme,

signals/lib/home/extra_actions_button.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ExtraActionsButton extends StatelessWidget {
2929
key: ArchSampleKeys.toggleAll,
3030
value: ExtraAction.toggleAllComplete,
3131
child: Text(
32-
controller.hasPendingTodos.value
32+
controller.hasActiveTodos.value
3333
? ArchSampleLocalizations.of(context).markAllComplete
3434
: ArchSampleLocalizations.of(context).markAllIncomplete,
3535
),

signals/lib/home/stats_view.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class StatsView extends StatelessWidget {
4343
padding: const EdgeInsets.only(bottom: 24.0),
4444
child: Watch(
4545
(context) => Text(
46-
'${controller.numPending}',
46+
'${controller.numActive}',
4747
key: ArchSampleKeys.statsNumActive,
4848
style: Theme.of(context).textTheme.titleMedium,
4949
),

signals/lib/todo_list_controller.dart

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,41 @@ enum VisibilityFilter { all, active, completed }
77

88
class TodoListController {
99
TodoListController({
10-
required TodosRepository todosRepository,
10+
required TodosRepository repository,
1111
VisibilityFilter? filter,
12-
List<Todo>? todos,
1312
TodoCodec? codec,
14-
}) : _todosRepository = todosRepository,
13+
}) : _todosRepository = repository,
1514
_todoCodec = codec ?? const TodoCodec(),
16-
todos = ListSignal(todos ?? []),
17-
filter = Signal(VisibilityFilter.all);
15+
todos = ListSignal([]),
16+
filter = Signal(filter ?? VisibilityFilter.all);
1817

1918
final TodosRepository _todosRepository;
2019
final TodoCodec _todoCodec;
2120
final ListSignal<Todo> todos;
2221
final Signal<VisibilityFilter> filter;
2322

24-
late final EffectCleanup _effectCleanup;
23+
late final EffectCleanup _persistenceEffectCleanup;
2524
late final Future<void> initializingFuture;
2625

27-
ReadonlySignal<List<Todo>> get pendingTodos => Computed(
26+
ReadonlySignal<List<Todo>> get activeTodos => Computed(
2827
() => todos.where((t) => !t.complete.value).toList(growable: false),
2928
);
3029

3130
ReadonlySignal<List<Todo>> get completedTodos => Computed(
3231
() => todos.where((t) => t.complete.value).toList(growable: false),
3332
);
3433

35-
ReadonlySignal<bool> get hasCompletedTodos =>
36-
Computed(() => completedTodos.value.isNotEmpty);
34+
ReadonlySignal<bool> get hasActiveTodos =>
35+
Computed(() => activeTodos.value.isNotEmpty);
3736

38-
ReadonlySignal<bool> get hasPendingTodos =>
39-
Computed(() => pendingTodos.value.isNotEmpty);
40-
41-
ReadonlySignal<int> get numPending =>
42-
Computed(() => pendingTodos.value.length);
37+
ReadonlySignal<int> get numActive => Computed(() => activeTodos.value.length);
4338

4439
ReadonlySignal<int> get numCompleted =>
4540
Computed(() => completedTodos.value.length);
4641

4742
ReadonlySignal<List<Todo>> get visibleTodos => Computed(
4843
() => switch (filter.value) {
49-
VisibilityFilter.active => pendingTodos.value,
44+
VisibilityFilter.active => activeTodos.value,
5045
VisibilityFilter.completed => completedTodos.value,
5146
VisibilityFilter.all => todos,
5247
},
@@ -55,9 +50,11 @@ class TodoListController {
5550
void toggleAll() {
5651
final allComplete = todos.every((todo) => todo.complete.value);
5752

58-
for (final todo in todos) {
59-
todo.complete.value = !allComplete;
60-
}
53+
batch(() {
54+
for (final todo in todos) {
55+
todo.complete.value = !allComplete;
56+
}
57+
});
6158
}
6259

6360
void clearCompleted() => todos.removeWhere((todo) => todo.complete.value);
@@ -80,11 +77,11 @@ class TodoListController {
8077
// to the repository more often than necessary. In production, save
8178
// operations are debounced by 500ms. In tests, they are not debounced to
8279
// speed up test execution.
83-
_effectCleanup = effect(() async {
84-
final toSave = todos.map(_todoCodec.encode).toList(growable: false);
80+
_persistenceEffectCleanup = effect(() async {
81+
final toSave = todos.value.map(_todoCodec.encode).toList(growable: false);
8582
await _todosRepository.saveTodos(toSave);
8683
});
8784
}
8885

89-
void dispose() => _effectCleanup();
86+
void dispose() => _persistenceEffectCleanup();
9087
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import 'package:mockito/annotations.dart';
2+
import 'package:mockito/mockito.dart';
3+
import 'package:signals_sample/todo.dart';
4+
import 'package:signals_sample/todo_list_controller.dart';
5+
import 'package:test/test.dart';
6+
import 'package:todos_repository_core/todos_repository_core.dart';
7+
8+
import 'todo_list_controller_test.mocks.dart';
9+
10+
@GenerateNiceMocks([MockSpec<TodosRepository>()])
11+
void main() {
12+
group('$TodoListController', () {
13+
test('should compute the number of completed todos', () async {
14+
final repository = MockTodosRepository();
15+
final controller = TodoListController(repository: repository);
16+
17+
when(repository.loadTodos()).thenAnswer(
18+
(_) async => [
19+
TodoEntity('a', '1', '', false),
20+
TodoEntity('b', '2', '', false),
21+
TodoEntity('c', '3', '', true),
22+
],
23+
);
24+
await controller.init();
25+
26+
expect(controller.numCompleted.value, 1);
27+
});
28+
29+
test('should calculate the number of active todos', () async {
30+
final repository = MockTodosRepository();
31+
final controller = TodoListController(repository: repository);
32+
33+
when(repository.loadTodos()).thenAnswer(
34+
(_) async => [
35+
TodoEntity('a', '1', '', false),
36+
TodoEntity('b', '2', '', false),
37+
TodoEntity('c', '3', '', true),
38+
],
39+
);
40+
await controller.init();
41+
42+
expect(controller.hasActiveTodos.value, isTrue);
43+
expect(controller.numActive.value, 2);
44+
});
45+
46+
test('should return all todos if the VisibilityFilter is all', () async {
47+
final repository = MockTodosRepository();
48+
final controller = TodoListController(
49+
filter: VisibilityFilter.all,
50+
repository: repository,
51+
);
52+
53+
when(repository.loadTodos()).thenAnswer(
54+
(_) async => [
55+
TodoEntity('a', '1', '', false),
56+
TodoEntity('b', '2', '', false),
57+
TodoEntity('c', '3', '', true),
58+
],
59+
);
60+
61+
await controller.init();
62+
63+
expect(controller.visibleTodos.value, [
64+
Todo('a', id: '1'),
65+
Todo('b', id: '2'),
66+
Todo('c', id: '3', complete: true),
67+
]);
68+
});
69+
70+
test(
71+
'should return active todos if the VisibilityFilter is active',
72+
() async {
73+
final repository = MockTodosRepository();
74+
final controller = TodoListController(
75+
filter: VisibilityFilter.active,
76+
repository: repository,
77+
);
78+
79+
when(repository.loadTodos()).thenAnswer(
80+
(_) async => [
81+
TodoEntity('a', '1', '', false),
82+
TodoEntity('b', '2', '', false),
83+
TodoEntity('c', '3', '', true),
84+
],
85+
);
86+
await controller.init();
87+
88+
expect(controller.visibleTodos.value, [
89+
Todo('a', id: '1'),
90+
Todo('b', id: '2'),
91+
]);
92+
},
93+
);
94+
95+
test(
96+
'should return completed todos if the VisibilityFilter is completed',
97+
() async {
98+
final repository = MockTodosRepository();
99+
final controller = TodoListController(
100+
filter: VisibilityFilter.completed,
101+
repository: repository,
102+
);
103+
104+
when(repository.loadTodos()).thenAnswer(
105+
(_) async => [
106+
TodoEntity('a', '1', '', false),
107+
TodoEntity('b', '2', '', false),
108+
TodoEntity('c', '3', '', true),
109+
],
110+
);
111+
await controller.init();
112+
113+
expect(controller.visibleTodos.value, [
114+
Todo('c', id: '3', complete: true),
115+
]);
116+
},
117+
);
118+
119+
test('should clear the completed todos', () async {
120+
final repository = MockTodosRepository();
121+
final controller = TodoListController(repository: repository);
122+
123+
when(repository.loadTodos()).thenAnswer(
124+
(_) async => [
125+
TodoEntity('a', '1', '', false),
126+
TodoEntity('b', '2', '', false),
127+
TodoEntity('c', '3', '', true),
128+
],
129+
);
130+
131+
await controller.init();
132+
controller.clearCompleted();
133+
134+
expect(controller.todos.value, [Todo('a', id: '1'), Todo('b', id: '2')]);
135+
verify(
136+
repository.saveTodos([
137+
TodoEntity('a', '1', '', false),
138+
TodoEntity('b', '2', '', false),
139+
]),
140+
);
141+
});
142+
143+
test('toggle all as complete or incomplete', () async {
144+
final repository = MockTodosRepository();
145+
final controller = TodoListController(repository: repository);
146+
147+
when(repository.loadTodos()).thenAnswer(
148+
(_) async => [
149+
TodoEntity('a', '1', '', false),
150+
TodoEntity('b', '2', '', false),
151+
TodoEntity('c', '3', '', true),
152+
],
153+
);
154+
155+
await controller.init();
156+
157+
// Toggle all complete
158+
controller.toggleAll();
159+
expect(controller.todos.every((t) => t.complete.value), isTrue);
160+
verify(
161+
repository.saveTodos([
162+
TodoEntity('a', '1', '', true),
163+
TodoEntity('b', '2', '', true),
164+
TodoEntity('c', '3', '', true),
165+
]),
166+
);
167+
168+
// Toggle all incomplete
169+
controller.toggleAll();
170+
expect(controller.todos.every((t) => !t.complete.value), isTrue);
171+
verify(
172+
repository.saveTodos([
173+
TodoEntity('a', '1', '', false),
174+
TodoEntity('b', '2', '', false),
175+
TodoEntity('c', '3', '', false),
176+
]),
177+
);
178+
});
179+
180+
test('should add a todo', () async {
181+
final repository = MockTodosRepository();
182+
final controller = TodoListController(repository: repository);
183+
184+
when(
185+
repository.loadTodos(),
186+
).thenAnswer((_) async => [TodoEntity('a', '1', '', false)]);
187+
188+
await controller.init();
189+
controller.todos.add(Todo('b', id: '2'));
190+
191+
expect(controller.todos, [Todo('a', id: '1'), Todo('b', id: '2')]);
192+
verify(
193+
repository.saveTodos([
194+
TodoEntity('a', '1', '', false),
195+
TodoEntity('b', '2', '', false),
196+
]),
197+
);
198+
});
199+
200+
test('should remove a todo', () async {
201+
final repository = MockTodosRepository();
202+
final controller = TodoListController(repository: repository);
203+
204+
when(
205+
repository.loadTodos(),
206+
).thenAnswer((_) async => [TodoEntity('a', '1', '', false)]);
207+
208+
await controller.init();
209+
210+
controller.todos.remove(Todo('a', id: '1'));
211+
212+
expect(controller.todos.value, <Todo>[]);
213+
verify(repository.saveTodos([]));
214+
});
215+
216+
test('should update a todo', () async {
217+
final repository = MockTodosRepository();
218+
final controller = TodoListController(repository: repository);
219+
220+
when(repository.loadTodos()).thenAnswer(
221+
(_) async => [
222+
TodoEntity('a', '1', '', false),
223+
TodoEntity('b', '2', '', false),
224+
TodoEntity('c', '3', '', true),
225+
],
226+
);
227+
await controller.init();
228+
229+
controller.todos[1].complete.value = true;
230+
231+
expect(controller.todos.value, [
232+
Todo('a', id: '1'),
233+
Todo('b', id: '2', complete: true),
234+
Todo('c', id: '3', complete: true),
235+
]);
236+
verify(
237+
repository.saveTodos([
238+
TodoEntity('a', '1', '', false),
239+
TodoEntity('b', '2', '', true),
240+
TodoEntity('c', '3', '', true),
241+
]),
242+
);
243+
});
244+
});
245+
}

0 commit comments

Comments
 (0)