test: create basic tests (#24)

Related to #1 but more tests should be added, so not closing

Reviewed-on: #24
This commit is contained in:
Matyáš Caras 2024-01-22 14:41:16 +01:00
parent dcc38645c8
commit 96e672f1d5
18 changed files with 276 additions and 124 deletions

1
.gitignore vendored
View file

@ -42,3 +42,4 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
reports

View file

@ -51,7 +51,7 @@ android {
applicationId "cafe.caras.prasule" applicationId "cafe.caras.prasule"
// You can update the following values to match your application needs. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@ -76,4 +76,6 @@ flutter {
source '../..' source '../..'
} }
dependencies {} dependencies {
implementation 'com.android.support:multidex:1.0.3'
}

View file

@ -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 { allprojects {
repositories { repositories {
google() google()

View file

@ -10,11 +10,17 @@ pluginManagement {
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
plugins { repositories {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 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"

View file

@ -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<int>).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),
);
});
});
}

View file

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

View file

@ -1,7 +1,11 @@
import 'dart:math';
import 'package:currency_picker/currency_picker.dart'; import 'package:currency_picker/currency_picker.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.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/recurring_entry.dart';
import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_entry.dart';
import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/api/wallet_manager.dart';
@ -132,7 +136,8 @@ class Wallet {
: recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day, : recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day,
); // add the variable again to check if we aren't missing any entries ); // add the variable again to check if we aren't missing any entries
logger.i( 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 WalletManager.saveWallet(this); // save wallet
} }
@ -158,6 +163,19 @@ class Wallet {
/// Empty wallet used for placeholders /// Empty wallet used for placeholders
static final Wallet empty = Wallet( static final Wallet empty = Wallet(
name: "Empty", name: "Empty",
entries: [],
recurringEntries: [],
categories: [
WalletCategory(
name: "Default",
id: 0,
icon: IconData(
Icons.payments.codePoint,
fontFamily: 'MaterialIcons',
),
color: Colors.white,
),
],
currency: Currency.from( currency: Currency.from(
json: { json: {
"code": "USD", "code": "USD",
@ -174,4 +192,66 @@ class Wallet {
}, },
), ),
); );
/// Creates test data used for debugging purposes
Future<void> 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);
}
} }

View file

@ -24,10 +24,23 @@ class WalletManager {
// TODO: do something with unreadable wallets // TODO: do something with unreadable wallets
} }
} }
logger.i(wallets.length);
return wallets; return wallets;
} }
/// Deletes all [Wallet]s
static Future<void> 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 /// Loads and returns a single [Wallet] by name
static Future<Wallet> loadWallet(String name) async { static Future<Wallet> loadWallet(String name) async {
final path = final path =

View file

@ -29,12 +29,15 @@ final logger = Logger();
/// The application itself /// The application itself
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
/// The application itself /// The application itself
const MyApp({super.key}); const MyApp({super.key, this.locale});
/// If Material You was applied /// If Material You was applied
/// ///
/// Used to check if it is supported /// Used to check if it is supported
static bool appliedYou = false; static bool appliedYou = false;
/// Override locale, used for testing
final Locale? locale;
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -50,6 +53,7 @@ class MyApp extends StatelessWidget {
...GlobalCupertinoLocalizations.delegates, ...GlobalCupertinoLocalizations.delegates,
], ],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: locale,
title: 'Prašule', title: 'Prašule',
theme: ThemeData( theme: ThemeData(
colorScheme: _materialYou colorScheme: _materialYou

View file

@ -522,6 +522,7 @@ class Indicator extends StatelessWidget {
/// Text shown next to the indicator circle /// Text shown next to the indicator circle
final String text; final String text;
/// Text style of the indicator
final TextStyle textStyle; final TextStyle textStyle;
@override @override

View file

@ -192,7 +192,9 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
onPressed: () { onPressed: () {
if (newEntry.data.name.isEmpty) { if (newEntry.data.name.isEmpty) {
showMessage( showMessage(
AppLocalizations.of(context).errorEmptyName, context); AppLocalizations.of(context).errorEmptyName,
context,
);
return; return;
} }
if (widget.editEntry != null) { if (widget.editEntry != null) {

View file

@ -315,7 +315,9 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
onPressed: () { onPressed: () {
if (newEntry.data.name.isEmpty) { if (newEntry.data.name.isEmpty) {
showMessage( showMessage(
AppLocalizations.of(context).errorEmptyName, context); AppLocalizations.of(context).errorEmptyName,
context,
);
return; return;
} }
if (widget.editEntry != null) { if (widget.editEntry != null) {

View file

@ -1,7 +1,6 @@
// ignore_for_file: inference_failure_on_function_invocation // ignore_for_file: inference_failure_on_function_invocation
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.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:intl/intl.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.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.dart';
import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_entry.dart';
import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/api/wallet_manager.dart';
@ -88,70 +86,7 @@ class _HomeViewState extends State<HomeView> {
onTap: () { onTap: () {
// debug option to quickly fill a wallet with data // debug option to quickly fill a wallet with data
if (selectedWallet == null) return; if (selectedWallet == null) return;
selectedWallet!.entries.clear(); selectedWallet!.createTestEntries().then((_) {
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) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
platformRoute( platformRoute(
(p0) => const HomeView(), (p0) => const HomeView(),
@ -555,6 +490,7 @@ class _HomeViewState extends State<HomeView> {
} }
description.write("${line.replaceAll(regex, "")}\n"); description.write("${line.replaceAll(regex, "")}\n");
} }
if (!ctx.mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
// show edit // show edit
final newEntry = final newEntry =

View file

@ -146,7 +146,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
(await SharedPreferences.getInstance()) (await SharedPreferences.getInstance())
.getBool("useMaterialYou") ?? .getBool("useMaterialYou") ??
false; false;
if (!mounted) return; if (!context.mounted) return;
await showDialog( await showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
@ -227,7 +227,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
await WalletManager.saveWallet( await WalletManager.saveWallet(
selectedWallet!, selectedWallet!,
); );
if (!mounted) return; if (!context.mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(

View file

@ -70,7 +70,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
final s = await SharedPreferences.getInstance(); final s = await SharedPreferences.getInstance();
await s.setInt("yearlygraph", 1); await s.setInt("yearlygraph", 1);
_yearly = 1; _yearly = 1;
if (!mounted) return; if (!ctx.mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
setState(() {}); setState(() {});
}, },
@ -90,7 +90,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
final s = await SharedPreferences.getInstance(); final s = await SharedPreferences.getInstance();
await s.setInt("yearlygraph", 2); await s.setInt("yearlygraph", 2);
_yearly = 2; _yearly = 2;
if (!mounted) return; if (!ctx.mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
setState(() {}); setState(() {});
}, },
@ -128,7 +128,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
final s = await SharedPreferences.getInstance(); final s = await SharedPreferences.getInstance();
await s.setInt("monthlygraph", 1); await s.setInt("monthlygraph", 1);
_monthly = 1; _monthly = 1;
if (!mounted) return; if (!ctx.mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
setState(() {}); setState(() {});
}, },
@ -148,7 +148,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
final s = await SharedPreferences.getInstance(); final s = await SharedPreferences.getInstance();
await s.setInt("monthlygraph", 2); await s.setInt("monthlygraph", 2);
_monthly = 2; _monthly = 2;
if (!mounted) return; if (!ctx.mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
setState(() {}); setState(() {});
}, },

View file

@ -75,7 +75,9 @@ class _TessdataListViewState extends State<TessdataListView> {
onPressed: () async { onPressed: () async {
await TessdataApi.deleteData(lang); await TessdataApi.deleteData(lang);
_tessdata[i][lang] = true; _tessdata[i][lang] = true;
if (mounted) Navigator.of(context).pop(); if (context.mounted) {
Navigator.of(context).pop();
}
}, },
), ),
PlatformButton( PlatformButton(

View file

@ -297,7 +297,7 @@ class _SetupViewState extends State<SetupView> {
(await SharedPreferences.getInstance()) (await SharedPreferences.getInstance())
.getBool("useMaterialYou") ?? .getBool("useMaterialYou") ??
false; false;
if (!mounted) return; if (!context.mounted) return;
await showDialog( await showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(

View file

@ -325,10 +325,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flex_color_picker name: flex_color_picker
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "3.3.1"
flex_seed_scheme: flex_seed_scheme:
dependency: transitive dependency: transitive
description: description: