From 96e672f1d5cbd2cbda8207241dea7b7e4ea74588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 14:41:16 +0100 Subject: [PATCH] test: create basic tests (#24) Related to #1 but more tests should be added, so not closing Reviewed-on: https://git.mnau.xyz/hernik/prasule/pulls/24 --- .gitignore | 1 + android/app/build.gradle | 6 +- android/build.gradle | 13 --- android/settings.gradle | 14 ++- integration_test/app_test.dart | 140 ++++++++++++++++++++++++ integration_test/setup_test.dart | 24 ---- lib/api/wallet.dart | 82 +++++++++++++- lib/api/wallet_manager.dart | 15 ++- lib/main.dart | 6 +- lib/util/graphs.dart | 1 + lib/views/create_entry.dart | 4 +- lib/views/create_recur_entry.dart | 4 +- lib/views/home.dart | 68 +----------- lib/views/settings/edit_categories.dart | 4 +- lib/views/settings/graph_type.dart | 8 +- lib/views/settings/tessdata_list.dart | 4 +- lib/views/setup.dart | 2 +- pubspec.lock | 4 +- 18 files changed, 276 insertions(+), 124 deletions(-) create mode 100644 integration_test/app_test.dart delete mode 100644 integration_test/setup_test.dart diff --git a/.gitignore b/.gitignore index 24476c5..1a1da39 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +reports \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 7551c3f..b3e80a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,7 +51,7 @@ android { applicationId "cafe.caras.prasule" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -76,4 +76,6 @@ flutter { source '../..' } -dependencies {} +dependencies { + implementation 'com.android.support:multidex:1.0.3' +} diff --git a/android/build.gradle b/android/build.gradle index f7eb7f6..bc157bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 55c4ca8..1d6d19b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -10,11 +10,17 @@ pluginManagement { includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + repositories { + google() + mavenCentral() + gradlePluginPortal() } } -include ":app" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart new file mode 100644 index 0000000..fd707c7 --- /dev/null +++ b/integration_test/app_test.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:logger/logger.dart'; +import 'package:prasule/api/category.dart'; +import 'package:prasule/api/entry_data.dart'; +import 'package:prasule/api/wallet.dart'; +import 'package:prasule/api/wallet_entry.dart'; +import 'package:prasule/api/wallet_manager.dart'; + +import 'package:prasule/main.dart'; +import 'package:prasule/pw/platformfield.dart'; + +void main() { + final logger = Logger(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group("Test classes and API", () { + test("Test wallet operations", () async { + expect( + (await WalletManager.listWallets()).length, + equals(0), + ); // check that there are no other wallets + await WalletManager.saveWallet(Wallet.empty); + var w = (await WalletManager.listWallets()).firstOrNull; + expect(w, isNotNull); // check that the wallet was successfully saved + expect(w!.categories.length, equals(1)); + w.categories.add( + WalletCategory( + name: "Testing", + id: w.nextCategoryId, + icon: Icons.abc, + color: Colors.orange, + ), + ); // create test category + final testId = w.nextId; + w.entries.add( + WalletSingleEntry( + data: EntryData(amount: 200, name: "Automated"), + type: EntryType.expense, + date: DateTime.now(), + category: w.categories.last, + id: w.nextId, + ), + ); // create test entry + await WalletManager.saveWallet(w); // save again + w = await WalletManager.loadWallet(w.name); // try loading manually + final e = w.entries.where((element) => element.id == testId).firstOrNull; + expect( + e, + isNotNull, + ); // check that the entry was successfully created + expect( + w.categories.where((element) => element.id == e!.category.id).length, + equals(1), + ); // check that the category exists too + + await WalletManager.deleteWallet(w); + expect( + (await WalletManager.listWallets()).length, + equals(0), + ); + }); + }); + + group("Test app functionality:", () { + testWidgets('First-time setup', (WidgetTester tester) async { + // Delete all data + await WalletManager.deleteAllData(); + // Build our app and trigger a frame. + await tester.pumpWidget( + const MyApp( + locale: Locale('en', 'US'), + ), + ); + await tester.pumpAndSettle(); + logger.i("Looking for welcome header"); + expect(find.text('Welcome!'), findsOneWidget); + + // Tap "Next" button + await tester.tap(find.text("Next")); + await tester.pumpAndSettle(); + + logger.i("Next view, looking for name+balance fields"); + + final firstFields = find.byType(PlatformField); + + expect(firstFields, findsExactly(2)); + + logger.i("Entering text"); + await tester.enterText(find.byType(PlatformField).first, "Debugging"); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(PlatformField).last, "100"); + await tester.pumpAndSettle(); + + // Tap "Next" button + await tester.tap(find.text("Next")); + await tester.pumpAndSettle(); + + // Tap "Finish" button + await tester.tap(find.text("Finish")); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is DropdownButton && + ((widget as DropdownButton).value ?? -1) == 0, + ), + findsOne, + ); + }); + + testWidgets('Test rendering of entries', (WidgetTester tester) async { + // Delete all data + await WalletManager.deleteAllData(); + expect((await WalletManager.listWallets()).length, equals(0)); + + // Create test data + final w = Wallet.empty; + await w.createTestEntries(); + expect((await WalletManager.listWallets()).length, equals(1)); + + // Build our app and trigger a frame. + await tester.pumpWidget( + const MyApp( + locale: Locale('en', 'US'), + ), + ); + await tester.pumpAndSettle(); + + // TODO: better test + + expect( + find.byType(ListTile, skipOffstage: false), + findsAtLeast(10), + ); + }); + }); +} diff --git a/integration_test/setup_test.dart b/integration_test/setup_test.dart deleted file mode 100644 index db9ca13..0000000 --- a/integration_test/setup_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:prasule/main.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group("Test Setup screen:", () { - testWidgets('First-time setup', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - expect(find.text('Welcome!'), findsOneWidget); - - // // Tap the '+' icon and trigger a frame. - // await tester.tap(find.byIcon(Icons.add)); - // await tester.pump(); - - // // Verify that our counter has incremented. - // expect(find.text('0'), findsNothing); - // expect(find.text('1'), findsOneWidget); - }); - }); -} diff --git a/lib/api/wallet.dart b/lib/api/wallet.dart index 077cea5..8440b51 100644 --- a/lib/api/wallet.dart +++ b/lib/api/wallet.dart @@ -1,7 +1,11 @@ +import 'dart:math'; + import 'package:currency_picker/currency_picker.dart'; +import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:prasule/api/category.dart'; +import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_manager.dart'; @@ -132,7 +136,8 @@ class Wallet { : recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day, ); // add the variable again to check if we aren't missing any entries logger.i( - "Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})"); + "Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})", + ); } WalletManager.saveWallet(this); // save wallet } @@ -158,6 +163,19 @@ class Wallet { /// Empty wallet used for placeholders static final Wallet empty = Wallet( name: "Empty", + entries: [], + recurringEntries: [], + categories: [ + WalletCategory( + name: "Default", + id: 0, + icon: IconData( + Icons.payments.codePoint, + fontFamily: 'MaterialIcons', + ), + color: Colors.white, + ), + ], currency: Currency.from( json: { "code": "USD", @@ -174,4 +192,66 @@ class Wallet { }, ), ); + + /// Creates test data used for debugging purposes + Future createTestEntries() async { + entries.clear(); + recurringEntries.clear(); + final random = Random(); + for (var i = 0; i < 30; i++) { + entries.add( + WalletSingleEntry( + data: EntryData( + name: "Test Entry #${i + 1}", + amount: random.nextInt(20000).toDouble(), + ), + type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income, + date: DateTime( + 2023, + random.nextInt(12) + 1, + random.nextInt(28) + 1, + ), + category: categories[random.nextInt(categories.length)], + id: nextId, + ), + ); + } + + logger.d( + "Created ${entries.length} regular entries", + ); + + for (var i = 0; i < 3; i++) { + final type = random.nextInt(3); + recurringEntries.add( + RecurringWalletEntry( + data: EntryData( + name: "Recurring Entry #${i + 1}", + amount: random.nextInt(20000).toDouble(), + ), + type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income, + date: DateTime( + 2023, + random.nextInt(12) + 1, + random.nextInt(28) + 1, + ), + category: categories[random.nextInt(categories.length)], + id: nextId, + lastRunDate: DateTime.now().subtract( + Duration( + days: (type > 0) ? 3 : 3 * 31, + ), + ), + recurType: (type > 0) ? RecurType.day : RecurType.month, + ), + ); + } + + logger.d( + "Created ${recurringEntries.length} recurring entries", + ); + + // save and reload + await WalletManager.saveWallet(this); + } } diff --git a/lib/api/wallet_manager.dart b/lib/api/wallet_manager.dart index e2c66ab..772c85a 100644 --- a/lib/api/wallet_manager.dart +++ b/lib/api/wallet_manager.dart @@ -24,10 +24,23 @@ class WalletManager { // TODO: do something with unreadable wallets } } - logger.i(wallets.length); return wallets; } + /// Deletes all [Wallet]s + static Future deleteAllData() async { + final path = + Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); + if (!path.existsSync()) { + return; + } + + for (final entry in path.listSync()) { + logger.d("Deleting ${entry.path}"); + entry.deleteSync(); + } + } + /// Loads and returns a single [Wallet] by name static Future loadWallet(String name) async { final path = diff --git a/lib/main.dart b/lib/main.dart index 9a86716..d6791ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,12 +29,15 @@ final logger = Logger(); /// The application itself class MyApp extends StatelessWidget { /// The application itself - const MyApp({super.key}); + const MyApp({super.key, this.locale}); /// If Material You was applied /// /// Used to check if it is supported static bool appliedYou = false; + + /// Override locale, used for testing + final Locale? locale; // This widget is the root of your application. @override Widget build(BuildContext context) { @@ -50,6 +53,7 @@ class MyApp extends StatelessWidget { ...GlobalCupertinoLocalizations.delegates, ], supportedLocales: AppLocalizations.supportedLocales, + locale: locale, title: 'PraĊĦule', theme: ThemeData( colorScheme: _materialYou diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index 2ccd5e5..211ee90 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -522,6 +522,7 @@ class Indicator extends StatelessWidget { /// Text shown next to the indicator circle final String text; + /// Text style of the indicator final TextStyle textStyle; @override diff --git a/lib/views/create_entry.dart b/lib/views/create_entry.dart index 1b27c8c..76320f9 100644 --- a/lib/views/create_entry.dart +++ b/lib/views/create_entry.dart @@ -192,7 +192,9 @@ class _CreateSingleEntryViewState extends State { onPressed: () { if (newEntry.data.name.isEmpty) { showMessage( - AppLocalizations.of(context).errorEmptyName, context); + AppLocalizations.of(context).errorEmptyName, + context, + ); return; } if (widget.editEntry != null) { diff --git a/lib/views/create_recur_entry.dart b/lib/views/create_recur_entry.dart index 1ad51a6..be03897 100644 --- a/lib/views/create_recur_entry.dart +++ b/lib/views/create_recur_entry.dart @@ -315,7 +315,9 @@ class _CreateRecurringEntryViewState extends State { onPressed: () { if (newEntry.data.name.isEmpty) { showMessage( - AppLocalizations.of(context).errorEmptyName, context); + AppLocalizations.of(context).errorEmptyName, + context, + ); return; } if (widget.editEntry != null) { diff --git a/lib/views/home.dart b/lib/views/home.dart index 6b31dac..a7606a4 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -1,7 +1,6 @@ // ignore_for_file: inference_failure_on_function_invocation import 'dart:async'; -import 'dart:math'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; @@ -16,7 +15,6 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/entry_data.dart'; -import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_manager.dart'; @@ -88,70 +86,7 @@ class _HomeViewState extends State { onTap: () { // debug option to quickly fill a wallet with data if (selectedWallet == null) return; - selectedWallet!.entries.clear(); - selectedWallet!.recurringEntries.clear(); - final random = Random(); - for (var i = 0; i < 30; i++) { - selectedWallet!.entries.add( - WalletSingleEntry( - data: EntryData( - name: "Test Entry #${i + 1}", - amount: random.nextInt(20000).toDouble(), - ), - type: (random.nextInt(3) > 0) - ? EntryType.expense - : EntryType.income, - date: DateTime( - 2023, - random.nextInt(12) + 1, - random.nextInt(28) + 1, - ), - category: selectedWallet!.categories[ - random.nextInt(selectedWallet!.categories.length)], - id: selectedWallet!.nextId, - ), - ); - } - - logger.d( - "Created ${selectedWallet!.entries.length} regular entries", - ); - - for (var i = 0; i < 3; i++) { - final type = random.nextInt(3); - selectedWallet!.recurringEntries.add( - RecurringWalletEntry( - data: EntryData( - name: "Recurring Entry #${i + 1}", - amount: random.nextInt(20000).toDouble(), - ), - type: (random.nextInt(3) > 0) - ? EntryType.expense - : EntryType.income, - date: DateTime( - 2023, - random.nextInt(12) + 1, - random.nextInt(28) + 1, - ), - category: selectedWallet!.categories[ - random.nextInt(selectedWallet!.categories.length)], - id: selectedWallet!.nextId, - lastRunDate: DateTime.now().subtract( - Duration( - days: (type > 0) ? 3 : 3 * 31, - ), - ), - recurType: (type > 0) ? RecurType.day : RecurType.month, - ), - ); - } - - logger.d( - "Created ${selectedWallet!.recurringEntries.length} recurring entries", - ); - - // save and reload - WalletManager.saveWallet(selectedWallet!).then((value) { + selectedWallet!.createTestEntries().then((_) { Navigator.of(context).pushReplacement( platformRoute( (p0) => const HomeView(), @@ -555,6 +490,7 @@ class _HomeViewState extends State { } description.write("${line.replaceAll(regex, "")}\n"); } + if (!ctx.mounted) return; Navigator.of(ctx).pop(); // show edit final newEntry = diff --git a/lib/views/settings/edit_categories.dart b/lib/views/settings/edit_categories.dart index 832234d..757c87b 100644 --- a/lib/views/settings/edit_categories.dart +++ b/lib/views/settings/edit_categories.dart @@ -146,7 +146,7 @@ class _EditCategoriesViewState extends State { (await SharedPreferences.getInstance()) .getBool("useMaterialYou") ?? false; - if (!mounted) return; + if (!context.mounted) return; await showDialog( context: context, builder: (c) => PlatformDialog( @@ -227,7 +227,7 @@ class _EditCategoriesViewState extends State { await WalletManager.saveWallet( selectedWallet!, ); - if (!mounted) return; + if (!context.mounted) return; Navigator.of(context).pop(); }, child: Text( diff --git a/lib/views/settings/graph_type.dart b/lib/views/settings/graph_type.dart index cc5d57e..2d7c48b 100644 --- a/lib/views/settings/graph_type.dart +++ b/lib/views/settings/graph_type.dart @@ -70,7 +70,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("yearlygraph", 1); _yearly = 1; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, @@ -90,7 +90,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("yearlygraph", 2); _yearly = 2; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, @@ -128,7 +128,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("monthlygraph", 1); _monthly = 1; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, @@ -148,7 +148,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("monthlygraph", 2); _monthly = 2; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, diff --git a/lib/views/settings/tessdata_list.dart b/lib/views/settings/tessdata_list.dart index a0648a5..855202c 100644 --- a/lib/views/settings/tessdata_list.dart +++ b/lib/views/settings/tessdata_list.dart @@ -75,7 +75,9 @@ class _TessdataListViewState extends State { onPressed: () async { await TessdataApi.deleteData(lang); _tessdata[i][lang] = true; - if (mounted) Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, ), PlatformButton( diff --git a/lib/views/setup.dart b/lib/views/setup.dart index e8749e3..cde8424 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -297,7 +297,7 @@ class _SetupViewState extends State { (await SharedPreferences.getInstance()) .getBool("useMaterialYou") ?? false; - if (!mounted) return; + if (!context.mounted) return; await showDialog( context: context, builder: (c) => PlatformDialog( diff --git a/pubspec.lock b/pubspec.lock index e57d14a..759eca0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,10 +325,10 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c + sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" flex_seed_scheme: dependency: transitive description: