Commit d77d831
Changed files (9)
app
integration_test
lib
model
blood_pressure
storage
test
docs
app/integration_test/add_measurement_test.dart
@@ -1,18 +1,24 @@
+import 'package:blood_pressure_app/app.dart';
import 'package:blood_pressure_app/components/dialoges/add_measurement_dialoge.dart';
import 'package:blood_pressure_app/components/measurement_list/measurement_list_entry.dart';
-import 'package:blood_pressure_app/main.dart' as app;
+import 'package:blood_pressure_app/components/settings/color_picker_list_tile.dart';
+import 'package:blood_pressure_app/screens/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
-void main() {
- final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+import '../test/ui/components/settings/color_picker_list_tile_test.dart';
+import 'util.dart';
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Can enter value only measurements', (WidgetTester tester) async {
+ await tester.pumpWidget(App(forceClearAppDataOnLaunch: true));
final localizations = await AppLocalizations.delegate.load(const Locale('en'));
- app.main();
await tester.pumpAndSettle();
+ await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+ expect(find.byType(AppHome), findsOneWidget);
expect(find.byType(AddEntryDialoge), findsNothing);
expect(find.byType(MeasurementListRow), findsNothing);
@@ -24,18 +30,13 @@ void main() {
await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
await tester.enterText(find.byType(TextFormField).at(2), '56'); // pul
-
+
await tester.tap(find.text(localizations.btnSave));
await tester.pumpAndSettle();
expect(find.byType(AddEntryDialoge), findsNothing);
- // Gets up to 5s to load from fs.
- int retries = 10;
- while(find.text(localizations.loading).hasFound && retries >= 0) {
- retries--;
- await tester.pump(Duration(milliseconds: 500));
- }
- await tester.pump();
+ await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
+ expect(find.text(localizations.loading), findsNothing);
expect(find.byType(MeasurementListRow), findsOneWidget);
expect(find.descendant(
@@ -52,4 +53,45 @@ void main() {
), findsOneWidget,);
});
+
+ testWidgets('Can enter complex measurements', (WidgetTester tester) async {
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+ await tester.pumpWidget(App(forceClearAppDataOnLaunch: true,));
+ await tester.pumpAndSettle();
+ await tester.pumpUntil(() => find.byType(AppHome).hasFound);
+ expect(find.byType(AppHome), findsOneWidget);
+
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialoge), findsOneWidget);
+
+ await tester.enterText(find.byType(TextFormField).at(0), '123'); // sys
+ await tester.enterText(find.byType(TextFormField).at(1), '67'); // dia
+ await tester.enterText(find.byType(TextFormField).at(2), '56'); // pul
+ await tester.enterText(find.byType(TextFormField).at(3), 'some test sample note'); // note
+ await tester.tap(find.byType(ColorSelectionListTile));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byElementPredicate(findColored(Colors.red)));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text(localizations.btnSave));
+ await tester.pumpAndSettle();
+ expect(find.byType(AddEntryDialoge), findsNothing);
+
+ await tester.pumpUntil(() => !find.text(localizations.loading).hasFound);
+ expect(find.text(localizations.loading), findsNothing);
+
+ expect(find.byType(MeasurementListRow), findsOneWidget);
+ final submittedRecord = tester.widget<MeasurementListRow>(find.byType(MeasurementListRow)).record;
+ expect(submittedRecord.systolic, 123);
+ expect(submittedRecord.diastolic, 67);
+ expect(submittedRecord.pulse, 56);
+ expect(submittedRecord.needlePin?.color.value, Colors.red.value);
+ expect(submittedRecord.notes, 'some test sample note');
+
+ expect(find.text('some test sample note'), findsNothing);
+ await tester.tap(find.byType(MeasurementListRow));
+ await tester.pumpAndSettle();
+ expect(find.text('some test sample note'), findsOneWidget);
+ });
}
app/integration_test/util.dart
@@ -0,0 +1,17 @@
+import 'package:flutter_test/flutter_test.dart';
+
+extension WaitUntil on WidgetTester {
+ /// Retries with 100ms delay for up to [maxLength] for a [test] to succeed.
+ ///
+ /// When no value is provided [maxLength] defaults to 5s.
+ Future<void> pumpUntil(bool Function() test, [Duration? maxLength]) async {
+ maxLength ??= Duration(seconds: 5);
+
+ int retries = maxLength.inMilliseconds ~/ 100;
+ while(!test() && retries >= 0) {
+ retries--;
+ await pump(Duration(milliseconds: 100));
+ }
+ await pump();
+ }
+}
\ No newline at end of file
app/lib/model/blood_pressure/model.dart
@@ -21,11 +21,9 @@ class BloodPressureModel extends ChangeNotifier {
Future<void> _asyncInit(String? dbPath, bool isFullPath) async {
dbPath ??= await getDatabasesPath();
-
if (dbPath != inMemoryDatabasePath && !isFullPath) {
dbPath = join(dbPath, 'blood_pressure.db');
}
-
// In case safer data loading is needed: finish this.
/*
String? backupPath;
@@ -44,6 +42,8 @@ class BloodPressureModel extends ChangeNotifier {
onUpgrade: _onDBUpgrade,
// When increasing the version an update procedure from every other possible version is needed
version: 2,
+ // In integration tests the file may be deleted which causes deadlocks.
+ singleInstance: false,
);
}
@@ -58,8 +58,8 @@ class BloodPressureModel extends ChangeNotifier {
// When adding more versions the upgrade procedure proposed in https://stackoverflow.com/a/75153875/21489239
// might be useful, to avoid duplicated code. Currently this would only lead to complexity, without benefits.
if (oldVersion == 1 && newVersion == 2) {
- db.execute('ALTER TABLE bloodPressureModel ADD COLUMN needlePin STRING;');
- db.database.setVersion(2);
+ await db.execute('ALTER TABLE bloodPressureModel ADD COLUMN needlePin STRING;');
+ await db.database.setVersion(2);
} else {
await ErrorReporting.reportCriticalError('Unsupported database upgrade', 'Attempted to upgrade the measurement database from version $oldVersion to version $newVersion, which is not supported. This action failed to avoid data loss. Please contact the app developer by opening an issue with the link below or writing an email to contact@derdilla.com.');
}
app/lib/model/storage/db/config_dao.dart
@@ -65,12 +65,12 @@ class ConfigDao {
Future<void> _updateSettings(int profileID, Settings settings) async {
if (!_configDB.database.isOpen) return;
await _configDB.database.insert(
- ConfigDB.settingsTable,
- {
- 'profile_id': profileID,
- 'settings_json': settings.toJson(),
- },
- conflictAlgorithm: ConflictAlgorithm.replace,
+ ConfigDB.settingsTable,
+ {
+ 'profile_id': profileID,
+ 'settings_json': settings.toJson(),
+ },
+ conflictAlgorithm: ConflictAlgorithm.replace,
);
}
app/lib/model/storage/db/config_db.dart
@@ -105,6 +105,8 @@ class ConfigDB {
onUpgrade: _onDBUpgrade,
// When increasing the version an update procedure from every other possible version is needed
version: 3,
+ // In integration tests the file may be deleted which causes deadlocks.
+ singleInstance: false,
);
}
app/lib/app.dart
@@ -60,19 +60,19 @@ class _AppState extends State<App> {
WidgetsFlutterBinding.ensureInitialized();
if (widget.forceClearAppDataOnLaunch) {
+ final dbPath = await getDatabasesPath();
try {
- final dbPath = await getDatabasesPath();
- File(join(await getDatabasesPath(), 'blood_pressure.db')).deleteSync();
- File(join(await getDatabasesPath(), 'blood_pressure.db-journal')).deleteSync();
+ File(join(dbPath, 'blood_pressure.db')).deleteSync();
+ File(join(dbPath, 'blood_pressure.db-journal')).deleteSync();
} on FileSystemException {
// File is likely already deleted or couldn't be created in the first place.
}
try {
- File(join(await getDatabasesPath(), 'config.db')).deleteSync();
- File(join(await getDatabasesPath(), 'config.db-journal')).deleteSync();
+ File(join(dbPath, 'config.db')).deleteSync();
+ File(join(dbPath, 'config.db-journal')).deleteSync();
} on FileSystemException { }
try {
- File(join(await getDatabasesPath(), 'medicine.intakes')).deleteSync();
+ File(join(dbPath, 'medicine.intakes')).deleteSync();
} on FileSystemException { }
}
app/test/ui/components/add_measurement_dialoge_test.dart
@@ -449,7 +449,6 @@ void main() {
expect(thirdFocusedTextFormField.evaluate().first.widget, isA<TextFormField>()
.having((p0) => p0.initialValue, 'note input content', 'note'),);
});
-
testWidgets('should focus last input field on backspace pressed in empty input field', (tester) async {
await loadDialoge(tester, (context) =>
showAddEntryDialoge(context, Settings(), mockRecord(sys: 12, dia: 3, pul: 4, note: 'note')),);
docs/testing.md
@@ -4,13 +4,28 @@ Testing means catching bugs early and automated testing has already prevented
multiple bugs from getting reintroduced. Therefor the goal is to have the
entire codebase covered by extensive tests.
-#### Running unit tests
+Integration
+
+### Unit tests
+
+Unit tests are fast and can all be run during development. Some util functions
+are present in the util file and in specialised ones in their respective widget
+test (e.g. color picker).
```bash
flutter test
```
-#### Running integration tests
+#### Integration test
+
+Integration tests are slow and mainly used for core workflows and things that
+can't be tested without them. Integration tests should not use the `main`
+method but should rather pump the App directly to allow tests to be independent
+of each other.
+
+```dart
+tester.pumpWidget(App(forceClearAppDataOnLaunch: true,));
+```
To run integration tests an android emulator needs to be running.