main
1import 'dart:async';
2
3import 'package:blood_pressure_app/model/storage/storage.dart';
4import 'package:flutter/material.dart';
5import 'package:flutter_bloc/flutter_bloc.dart';
6import 'package:blood_pressure_app/l10n/app_localizations.dart';
7import 'package:flutter_test/flutter_test.dart';
8import 'package:health_data_store/health_data_store.dart';
9import 'package:path/path.dart';
10import 'package:provider/provider.dart';
11
12/// Create a root material widget with localizations.
13Widget materialApp(Widget child, {
14 Settings? settings,
15 ExportSettings? exportSettings,
16 CsvExportSettings? csvExportSettings,
17 PdfExportSettings? pdfExportSettings,
18 IntervalStoreManager? intervallStoreManager,
19}) {
20 settings ??= Settings();
21 exportSettings ??= ExportSettings();
22 csvExportSettings ??= CsvExportSettings();
23 pdfExportSettings ??= PdfExportSettings();
24 intervallStoreManager ??= IntervalStoreManager(IntervalStorage(), IntervalStorage(), IntervalStorage());
25 return MultiProvider(
26 providers: [
27 ChangeNotifierProvider.value(value: settings),
28 ChangeNotifierProvider.value(value: exportSettings),
29 ChangeNotifierProvider.value(value: csvExportSettings),
30 ChangeNotifierProvider.value(value: pdfExportSettings),
31 ChangeNotifierProvider.value(value: intervallStoreManager),
32 ],
33 child: MaterialApp(
34 localizationsDelegates: AppLocalizations.localizationsDelegates,
35 locale: const Locale('en'),
36 home: Scaffold(body:child),
37 ),
38 );
39}
40
41/// Creates a the same App as the main method.
42Widget appBase(Widget child, {
43 Settings? settings,
44 ExportSettings? exportSettings,
45 CsvExportSettings? csvExportSettings,
46 PdfExportSettings? pdfExportSettings,
47 IntervalStoreManager? intervallStoreManager,
48 BloodPressureRepository? bpRepo,
49 MedicineRepository? medRepo,
50 NoteRepository? noteRepo,
51 MedicineIntakeRepository? intakeRepo,
52 BodyweightRepository? weightRepo,
53}) {
54 HealthDataStore? db;
55 if (bpRepo == null
56 || medRepo == null
57 || intakeRepo == null
58 || noteRepo == null
59 || weightRepo == null
60 ) {
61 db = MockHealthDataSore();
62 }
63
64 return MultiRepositoryProvider(
65 providers: [
66 RepositoryProvider(create: (context) => bpRepo ?? db!.bpRepo),
67 RepositoryProvider(create: (context) => medRepo ?? db!.medRepo),
68 RepositoryProvider(create: (context) => intakeRepo ?? db!.intakeRepo),
69 RepositoryProvider(create: (context) => noteRepo ?? db!.noteRepo),
70 RepositoryProvider(create: (context) => weightRepo ?? db!.weightRepo),
71 ],
72 child: materialApp(child,
73 settings: settings,
74 exportSettings: exportSettings,
75 csvExportSettings: csvExportSettings,
76 pdfExportSettings: pdfExportSettings,
77 intervallStoreManager: intervallStoreManager,
78 ),
79 );
80}
81
82/// Creates a the same App as the main method.
83Future<Widget> appBaseWithData(Widget child, {
84 Settings? settings,
85 ExportSettings? exportSettings,
86 CsvExportSettings? csvExportSettings,
87 PdfExportSettings? pdfExportSettings,
88 IntervalStoreManager? intervallStoreManager,
89 List<BloodPressureRecord>? records,
90 List<Medicine>? meds,
91 List<Note>? notes,
92 List<MedicineIntake>? intakes,
93 List<BodyweightRecord>? weights,
94}) async {
95 final db = MockHealthDataSore();
96 final bpRepo = db.bpRepo;
97 for (final r in records ?? []) {
98 await bpRepo.add(r);
99 }
100 final medRepo = db.medRepo;
101 for (final m in meds ?? []) {
102 await medRepo.add(m);
103 }
104 final intakeRepo = db.intakeRepo;
105 for (final i in intakes ?? []) {
106 await intakeRepo.add(i);
107 }
108 final noteRepo = db.noteRepo;
109 for (final n in notes ?? []) {
110 await noteRepo.add(n);
111 }
112 final weightRepo = db.weightRepo;
113 for (final w in weights ?? []) {
114 await weightRepo.add(w);
115 }
116
117 return appBase(
118 child,
119 settings: settings,
120 exportSettings: exportSettings,
121 csvExportSettings: csvExportSettings,
122 pdfExportSettings: pdfExportSettings,
123 intervallStoreManager: intervallStoreManager,
124 bpRepo: bpRepo,
125 medRepo: medRepo,
126 noteRepo: noteRepo,
127 intakeRepo: intakeRepo,
128 weightRepo: weightRepo,
129 );
130}
131
132/// [materialApp] variant that doesn't assume scaffold.
133Widget materialForScreens(Widget child, {
134 Settings? settings,
135 ExportSettings? exportSettings,
136 CsvExportSettings? csvExportSettings,
137 PdfExportSettings? pdfExportSettings,
138 IntervalStoreManager? intervallStoreManager,
139}) {
140 settings ??= Settings();
141 exportSettings ??= ExportSettings();
142 csvExportSettings ??= CsvExportSettings();
143 pdfExportSettings ??= PdfExportSettings();
144 intervallStoreManager ??= IntervalStoreManager(IntervalStorage(), IntervalStorage(), IntervalStorage());
145 return MultiProvider(
146 providers: [
147 ChangeNotifierProvider.value(value: settings),
148 ChangeNotifierProvider.value(value: exportSettings),
149 ChangeNotifierProvider.value(value: csvExportSettings),
150 ChangeNotifierProvider.value(value: pdfExportSettings),
151 ChangeNotifierProvider.value(value: intervallStoreManager),
152 ],
153 child: MaterialApp(
154 localizationsDelegates: AppLocalizations.localizationsDelegates,
155 locale: const Locale('en'),
156 home: child,
157 ),
158 );
159}
160
161Widget appBaseForScreen(Widget child, {
162 Settings? settings,
163 ExportSettings? exportSettings,
164 CsvExportSettings? csvExportSettings,
165 PdfExportSettings? pdfExportSettings,
166 IntervalStoreManager? intervallStoreManager,
167 BloodPressureRepository? bpRepo,
168 MedicineRepository? medRepo,
169 NoteRepository? noteRepo,
170 MedicineIntakeRepository? intakeRepo,
171 BodyweightRepository? weightRepo,
172}) {
173 HealthDataStore? db;
174 if (bpRepo == null
175 || medRepo == null
176 || intakeRepo == null
177 || noteRepo == null
178 || weightRepo == null
179 ) {
180 db = MockHealthDataSore();
181 }
182
183 return MultiRepositoryProvider(
184 providers: [
185 RepositoryProvider(create: (context) => bpRepo ?? db!.bpRepo),
186 RepositoryProvider(create: (context) => medRepo ?? db!.medRepo),
187 RepositoryProvider(create: (context) => intakeRepo ?? db!.intakeRepo),
188 RepositoryProvider(create: (context) => noteRepo ?? db!.noteRepo),
189 RepositoryProvider(create: (context) => weightRepo ?? db!.weightRepo),
190 ],
191 child: materialForScreens(child,
192 settings: settings,
193 exportSettings: exportSettings,
194 csvExportSettings: csvExportSettings,
195 pdfExportSettings: pdfExportSettings,
196 intervallStoreManager: intervallStoreManager,
197 ),
198 );
199}
200
201/// Open a dialoge through a button press.
202///
203/// Example usage:
204/// ```dart
205/// dynamic returnedValue = false;
206/// await loadDialoge(tester, (context) async => returnedValue =
207/// await showAddExportColumnDialoge(context, Settings(),
208/// UserColumn('initialInternalIdentifier', 'csvTitle', 'formatPattern')
209/// ));
210/// ```
211Future<void> loadDialoge(WidgetTester tester, void Function(BuildContext context) dialogeStarter, {
212 String dialogeStarterText = 'X',
213 Settings? settings,
214}) async {
215 await tester.pumpWidget(materialApp(
216 Builder(builder: (context) => TextButton(onPressed: () => dialogeStarter(context), child: Text(dialogeStarterText)),),
217 settings: settings,
218 ),);
219 await tester.tap(find.text(dialogeStarterText));
220 await tester.pumpAndSettle();
221}
222
223/// Get empty mock med repo.
224// Using a instance of the real repository somehow causes a deadlock in tests.
225MedicineRepository medRepo([List<Medicine>? meds]) => MockMedRepo(meds);
226
227class MockMedRepo implements MedicineRepository {
228 MockMedRepo(List<Medicine>? meds) {
229 if (meds != null) _meds.addAll(meds);
230 }
231
232 final List<Medicine> _meds = [];
233
234 final _controller = StreamController.broadcast();
235
236 @override
237 Future<void> add(Medicine medicine) async {
238 _meds.add(medicine);
239 _controller.add(null);
240 }
241
242 @override
243 Future<List<Medicine>> getAll() async=> _meds;
244
245 @override
246 Future<void> remove(Medicine value) async {
247 _meds.remove(value);
248 _controller.add(null);
249 }
250
251 @override
252 @Deprecated('Medicines have no date. Use getAll directly')
253 Future<List<Medicine>> get(DateRange range) => getAll();
254
255 @override
256 Stream subscribe() => _controller.stream;
257}
258
259final List<Medicine> _meds = [];
260
261/// Creates mock Medicine.
262///
263/// Medicines with the same properties will keep the correct id.
264Medicine mockMedicine({
265 Color color = Colors.black,
266 String designation = '',
267 double? defaultDosis,
268}) {
269 final matchingMeds = _meds.where((med) => med.dosis?.mg == defaultDosis
270 && med.color == color.toARGB32()
271 && med.designation == designation,
272 );
273 if (matchingMeds.isNotEmpty) return matchingMeds.first;
274 final med = Medicine(
275 designation: designation,
276 color: color.toARGB32(),
277 dosis: defaultDosis == null ? null : Weight.mg(defaultDosis),
278 );
279 _meds.add(med);
280 return med;
281}
282
283class MockHealthDataSore implements HealthDataStore {
284 @override
285 BloodPressureRepository bpRepo = MockBloodPressureRepository();
286
287 @override
288 MedicineIntakeRepository intakeRepo = MockMedicineIntakeRepository();
289
290 @override
291 MedicineRepository medRepo = MockMedicineRepository();
292
293 @override
294 NoteRepository noteRepo = MockNoteRepository();
295
296 @override
297 BodyweightRepository weightRepo = MockBodyweightRepository();
298}
299
300class _MockRepo<T> extends Repository<T> {
301 List<T> data = [];
302 final contr = StreamController.broadcast();
303
304 @override
305 Future<void> add(T value) async {
306 data.add(value);
307 contr.sink.add(null);
308 }
309
310 @override
311 Future<List<T>> get(DateRange range) async => data;
312
313 @override
314 Future<void> remove(T value) async {
315 data.remove(value);
316 contr.sink.add(null);
317 }
318
319 @override
320 Stream subscribe() => contr.stream;
321
322 @override
323 dynamic noSuchMethod(Invocation invocation) => throw Exception('unexpected call: $invocation');
324}
325
326class MockBloodPressureRepository extends _MockRepo<BloodPressureRecord> implements BloodPressureRepository {}
327class MockMedicineIntakeRepository extends _MockRepo<MedicineIntake> implements MedicineIntakeRepository {}
328class MockMedicineRepository extends _MockRepo<Medicine> implements MedicineRepository {}
329class MockNoteRepository extends _MockRepo<Note> implements NoteRepository {}
330class MockBodyweightRepository extends _MockRepo<BodyweightRecord> implements BodyweightRepository {}
331
332/// [matchesGoldenFile] wrapper that includes a dir for image names.
333dynamic myMatchesGoldenFile(String key) => matchesGoldenFile(join('golden', key));