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));