Added tests, updated widget structure

main
henryhdr 2024-06-12 12:48:30 +02:00
parent 880696ad18
commit a9e7380d69
14 changed files with 1070 additions and 488 deletions

View File

@ -1,4 +1,10 @@
enum PayoutInterval{ enum PayoutInterval{
yearly, yearly,
monthly monthly
}
enum CalculationPerformed{
noFirstTimeItLoaded,
no,
yes
} }

View File

@ -6,6 +6,7 @@ import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/input_widget.dart'; import 'package:flutter_application_1/widgets/input_widget.dart';
import 'package:flutter_application_1/widgets/interval_widget.dart'; import 'package:flutter_application_1/widgets/interval_widget.dart';
import 'package:flutter_application_1/widgets/result_widget.dart'; import 'package:flutter_application_1/widgets/result_widget.dart';
import 'package:flutter_application_1/widgets/error_widget.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@ -17,7 +18,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Zinseszinsrechner',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: CupertinoColors.white, background: CupertinoColors.white), colorScheme: ColorScheme.fromSeed(seedColor: CupertinoColors.white, background: CupertinoColors.white),
useMaterial3: true, useMaterial3: true,
@ -157,7 +158,7 @@ class _MyHomePageState extends State<MyHomePage> {
}); });
} }
bool calculationPerformed = false; CalculationPerformed _isCalculated = CalculationPerformed.noFirstTimeItLoaded;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -170,10 +171,38 @@ class _MyHomePageState extends State<MyHomePage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
buildInputWidget('Anfangskapital', _initialCapitalController, _initialCapitalFocusNode, _isInitialCapitalEntered, '', 'Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.'), InputWidget(
buildInputWidget('Monatliche Sparrate', _monthlySavingsRateController, _monthlySavingsRateFocusNode, _isMonthlySavingsRateEntered, '', 'Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.'), label: 'Anfangskapital',
buildInputWidget('Jährlicher Zinssatz', _interestRateController, _interestRateFocusNode, _isInterestRateEntered, '%', 'Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.'), controller: _initialCapitalController,
buildInputWidget('Anlagezeitraum', _timeController, _timeFocusNode, _isTimeEntered, 'Jahre', 'Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.'), focusNode: _initialCapitalFocusNode,
isValid: _isInitialCapitalEntered,
suffixText: '',
tooltipText: 'Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.'
),
InputWidget(
label: 'Monatliche Sparrate',
controller: _monthlySavingsRateController,
focusNode: _monthlySavingsRateFocusNode,
isValid: _isMonthlySavingsRateEntered,
suffixText: '',
tooltipText: 'Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.'
),
InputWidget(
label: 'Jährlicher Zinssatz',
controller: _interestRateController,
focusNode: _interestRateFocusNode,
isValid: _isInterestRateEntered,
suffixText: '%',
tooltipText: 'Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.'
),
InputWidget(
label: 'Anlagezeitraum',
controller: _timeController,
focusNode: _timeFocusNode,
isValid: _isTimeEntered,
suffixText: 'Jahre',
tooltipText: 'Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.'
),
IntervalWidget( IntervalWidget(
selectedInterval: translateInterval(_payoutInterval), selectedInterval: translateInterval(_payoutInterval),
onChanged: (newInterval) { onChanged: (newInterval) {
@ -184,13 +213,29 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
setInitialCapital(); if (_isInitialCapitalEntered &&
setMonthlySavingsRate(); _isMonthlySavingsRateEntered &&
setInterestRate(); _isInterestRateEntered &&
setTime(); _isTimeEntered) {
_investedMoney = calculateInvestedMoney(_initialCapital, _monthlySavingsRate, _time, _investedMoneyList); setInitialCapital();
_compoundInterest = calculateCompoundInterest(_initialCapital, _monthlySavingsRate, _interestRate, _time, _payoutInterval, _investedMoneyList, _compoundInterestList); setMonthlySavingsRate();
calculationPerformed = true; setInterestRate();
setTime();
_investedMoney = calculateInvestedMoney(_initialCapital, _monthlySavingsRate, _time, _investedMoneyList);
_compoundInterest = calculateCompoundInterest(
_initialCapital,
_monthlySavingsRate,
_interestRate,
_time,
_payoutInterval,
_investedMoneyList,
_compoundInterestList
);
_isCalculated = CalculationPerformed.yes;
} else {
_isCalculated = CalculationPerformed.no;
}
setState(() {});
}, },
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(CupertinoColors.black), backgroundColor: MaterialStateProperty.all<Color>(CupertinoColors.black),
@ -209,8 +254,21 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
if (calculationPerformed) if(_isCalculated == CalculationPerformed.yes)
buildResultWidget(context, '$_compoundInterest', '$_investedMoney', '$_time', '$_monthlySavingsRate', '$_interestRate', _payoutInterval, _investedMoneyList, _compoundInterestList), ResultWidget(
compoundInterest: '$_compoundInterest',
investedMoney: '$_investedMoney',
time: '$_time',
monthlySavingsRate: '$_monthlySavingsRate',
interestRate: '$_interestRate',
payoutInterval: _payoutInterval,
investedMoneyList: _investedMoneyList,
compoundInterestList: _compoundInterestList,
),
if(_isCalculated == CalculationPerformed.no)
const ErrWidget(
errorMessage: 'Ungültige Eingabe',
),
], ],
), ),
) )

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
// Widget für die Seite, die das gestapelte Säulendiagramm anzeigt
class ChartPage extends StatelessWidget {
final List<double> investedMoneyList; // Liste der investierten Geldbeträge
final List<double> compoundInterestList; // Liste der zusammengesetzten Zinsen
const ChartPage({
super.key,
required this.investedMoneyList,
required this.compoundInterestList,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(
left: 10,
right: 10,
top: MediaQuery.of(context).padding.top + 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15),
onPressed: () {
Navigator.pop(context); // Zurück zur vorherigen Seite
},
),
const Text(
'Grafik',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40),
],
),
),
),
// Anzeige des gestapelten Säulendiagramms
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: StackedColumnChart(
lowerValues: investedMoneyList,
upperValues: compoundInterestList,
),
),
),
// Tabelle mit Einzahlungen, Zinsen und Endkapital
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.circular(5),
),
clipBehavior: Clip.antiAlias,
child: Table(
columnWidths: const {
0: FixedColumnWidth(60),
1: FlexColumnWidth(),
2: FlexColumnWidth(),
3: FlexColumnWidth(),
},
border: TableBorder.symmetric(
inside: const BorderSide(
color: CupertinoColors.white,
width: 1,
),
),
children: [
// Tabellenkopf
const TableRow(
decoration: BoxDecoration(
color: CupertinoColors.darkBackgroundGray,
),
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Jahr',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Einzahlungen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Zinsen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Endkapital',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
],
),
// Datenzeilen
for (int i = 0; i < investedMoneyList.length; i++)
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${i + 1}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i] + investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
],
),
],
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
// Widget für die Seite, die die Meilenstein-Timeline anzeigt
class MilestonePage extends StatelessWidget {
final String compoundInterest; // Gesamte zusammengesetzte Zinsen
final String investedMoney; // Gesamte investierte Geldmenge
final List<Map<String, dynamic>> milestoneList; // Liste von Meilensteinen
const MilestonePage({
super.key,
required this.compoundInterest,
required this.investedMoney,
required this.milestoneList,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15),
onPressed: () {
Navigator.pop(context); // Zurück zur vorherigen Seite
},
),
const Text(
'Meilensteine',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40),
],
),
),
),
// Anzeige der Meilenstein-Timeline
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: MilestoneTimeline(milestones: milestoneList, totalInterest: double.parse(compoundInterest) - double.parse(investedMoney)),
),
),
],
),
);
}
}

View File

@ -22,6 +22,7 @@ void restoreDefaultValuesIfEmpty(TextEditingController controller) {
} }
} }
// Übersetzt das Auszahlungsintervall
String translateInterval(PayoutInterval interval) { String translateInterval(PayoutInterval interval) {
switch (interval) { switch (interval) {
case PayoutInterval.yearly: case PayoutInterval.yearly:
@ -29,4 +30,4 @@ String translateInterval(PayoutInterval interval) {
case PayoutInterval.monthly: case PayoutInterval.monthly:
return 'monatlich'; return 'monatlich';
} }
} }

View File

@ -1,8 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:syncfusion_flutter_charts/charts.dart';
// Widget zur Erstellung eines gestapelten Säulendiagramms // Widget für die Erstellung eines gestapelten Säulendiagramms
class StackedColumnChart extends StatelessWidget { class StackedColumnChart extends StatelessWidget {
final List<double> lowerValues; // Liste der unteren Werte für das Diagramm final List<double> lowerValues; // Liste der unteren Werte für das Diagramm
final List<double> upperValues; // Liste der oberen Werte für das Diagramm final List<double> upperValues; // Liste der oberen Werte für das Diagramm
@ -44,7 +43,6 @@ class StackedColumnChart extends StatelessWidget {
color: CupertinoColors.systemGreen.highContrastColor, color: CupertinoColors.systemGreen.highContrastColor,
), ),
], ],
// Konfiguration für die Interaktivität des Diagramms
trackballBehavior: TrackballBehavior( trackballBehavior: TrackballBehavior(
enable: true, enable: true,
tooltipSettings: const InteractiveTooltip(enable: true), tooltipSettings: const InteractiveTooltip(enable: true),
@ -75,160 +73,8 @@ class StackedColumnChart extends StatelessWidget {
// Klasse zur Modellierung der Datenpunkte im Diagramm // Klasse zur Modellierung der Datenpunkte im Diagramm
class SalesData { class SalesData {
final int year; final int year; // Jahr des Datenpunkts
final double value; final double value; // Wert des Datenpunkts
SalesData(this.year, this.value); SalesData(this.year, this.value);
} }
// Widget, welches das Diagramm auf eine neue Seite auslagert
@override
Widget buildChartPage(BuildContext context, List<double> investedMoneyList, List<double> compoundInterestList) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15), // Zurück-Pfeil
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Grafik',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40), // Platzhalter für zentrierte Ausrichtung
],
),
),
),
SliverToBoxAdapter( // Diagramm Anzeigen
child: Padding(
padding: const EdgeInsets.all(10.0),
child: StackedColumnChart(
lowerValues: investedMoneyList,
upperValues: compoundInterestList,
),
),
),
SliverToBoxAdapter( // Tabelle mit allen Werten anzeigen
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.circular(5),
),
clipBehavior: Clip.antiAlias,
child: Table(
columnWidths: const {
0: FixedColumnWidth(60), // Spaltenbreite für die erste Spalte verringern
1: FlexColumnWidth(),
2: FlexColumnWidth(),
3: FlexColumnWidth(),
},
border: TableBorder.symmetric(
inside: const BorderSide(color: CupertinoColors.white, width: 1),
),
children: [
const TableRow(
decoration: BoxDecoration(color: CupertinoColors.darkBackgroundGray),
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Jahr',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Einzahlungen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Zinsen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Endkapital',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
],
),
for (int i = 0; i < investedMoneyList.length; i++)
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${i + 1}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i] + investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
],
),
],
),
),
),
),
],
),
);
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Widget für eine benutzerdefinierten Button mit einem Hintergrundbild
class CustomImageButton extends StatelessWidget { class CustomImageButton extends StatelessWidget {
final VoidCallback onPressed; final VoidCallback onPressed; // Callback-Funktion für den Tastendruck
final Widget child; final Widget child; // Das Widget, das innerhalb der Schaltfläche angezeigt werden soll
final String backgroundImage; final String backgroundImage; // Pfad zum Hintergrundbild
const CustomImageButton({ const CustomImageButton({
super.key, super.key,

View File

@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
// Klasse für die Erstellung eines Fehler-Widgets mit einer Fehlermeldung
class ErrWidget extends StatelessWidget {
final String errorMessage; // Fehlermeldung, die angezeigt werden soll
const ErrWidget({super.key, required this.errorMessage});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.exclamationmark_circle_fill,
color: CupertinoColors.systemRed,
),
const SizedBox(width: 8),
Text(
errorMessage,
style: const TextStyle(color: CupertinoColors.systemRed),
),
],
);
}
}

View File

@ -1,81 +1,103 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Erstellt ein Widget für die Eingabe mit einem Label, einem Texteingabefeld, einer Validierungsanzeige und einem Tooltip // Widget zur Eingabe mit einem Label, einem Texteingabefeld, einer Validierungsanzeige und einem Tooltip
Widget buildInputWidget(String label, TextEditingController controller, FocusNode focusNode, bool isValid, String suffixText, String tooltipText) { class InputWidget extends StatefulWidget {
return Column( final String label; // Beschriftung des Eingabefelds
crossAxisAlignment: CrossAxisAlignment.start, final TextEditingController controller; // Controller für die Texteingabe
children: [ final FocusNode focusNode; // FocusNode für die Eingabe
Row( final bool isValid; // Validierungsstatus
mainAxisAlignment: MainAxisAlignment.spaceBetween, final String suffixText; // Suffix-Text für das Eingabefeld
children: [ final String tooltipText; // Erklärungstext im Tooltip
Row(
children: [ const InputWidget({
Row( super.key,
children: [ required this.label,
// Label für das Eingabefeld required this.controller,
Text( required this.focusNode,
label, required this.isValid,
style: const TextStyle( required this.suffixText,
fontWeight: FontWeight.bold, required this.tooltipText,
fontSize: 16, });
),
@override
InputWidgetState createState() => InputWidgetState();
}
class InputWidgetState extends State<InputWidget> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
widget.label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
), ),
const SizedBox(width: 5), ),
// Tooltip-Icon mit Erklärungstext const SizedBox(width: 5),
Tooltip( // Tooltip mit Erklärungstext
message: tooltipText, Tooltip(
triggerMode: TooltipTriggerMode.tap, message: widget.tooltipText,
decoration: const BoxDecoration( triggerMode: TooltipTriggerMode.tap,
color: CupertinoColors.black, decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5)) color: CupertinoColors.black,
), borderRadius: BorderRadius.all(Radius.circular(5)),
textStyle: const TextStyle(
color: CupertinoColors.white,
),
margin: const EdgeInsets.all(20),
child: const Icon(CupertinoIcons.question_circle_fill, size: 15),
), ),
], textStyle: const TextStyle(
color: CupertinoColors.white,
),
margin: const EdgeInsets.all(20),
child: const Icon(
CupertinoIcons.question_circle_fill,
size: 15,
),
),
],
),
// Icon zur Anzeige der Validierung
widget.isValid
? const Icon(
CupertinoIcons.check_mark_circled_solid,
color: CupertinoColors.systemGreen,
size: 15,
)
: const Icon(
CupertinoIcons.clear_circled_solid,
color: CupertinoColors.systemRed,
size: 15,
),
],
),
const SizedBox(height: 5),
CupertinoTextField(
controller: widget.controller,
focusNode: widget.focusNode,
keyboardType: TextInputType.number,
suffix: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Text(
widget.suffixText,
style: const TextStyle(
fontWeight: FontWeight.bold,
), ),
],
),
// Icon zur Anzeige der Validierung (grün für gültig, rot für ungültig)
isValid
? const Icon(
CupertinoIcons.check_mark_circled_solid,
color: CupertinoColors.systemGreen,
size: 15,
)
: const Icon(
CupertinoIcons.clear_circled_solid,
color: CupertinoColors.systemRed,
size: 15,
),
],
),
const SizedBox(height: 5),
// Texteingabefeld mit Suffix-Text
CupertinoTextField(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.number,
suffix: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Text(
suffixText,
style: const TextStyle(
fontWeight: FontWeight.bold,
), ),
), ),
decoration: BoxDecoration(
color: CupertinoColors.extraLightBackgroundGray,
borderRadius: BorderRadius.circular(5),
),
cursorColor: CupertinoColors.black,
), ),
decoration: BoxDecoration( const SizedBox(height: 20),
color: CupertinoColors.extraLightBackgroundGray, ],
borderRadius: BorderRadius.circular(5), );
), }
cursorColor: CupertinoColors.black,
),
const SizedBox(height: 20),
],
);
} }

View File

@ -3,9 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart'; import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/utils.dart'; import 'package:flutter_application_1/utils.dart';
// Widget zur Auswahl des Ausschüttungsintervalls
class IntervalWidget extends StatefulWidget { class IntervalWidget extends StatefulWidget {
final String selectedInterval; final String selectedInterval; // Das aktuell ausgewählte Intervall
final Function(String) onChanged; final Function(String) onChanged; // Funktion, die aufgerufen wird, wenn das Intervall geändert wird
const IntervalWidget({ const IntervalWidget({
super.key, super.key,
@ -51,77 +52,94 @@ class IntervalWidgetState extends State<IntervalWidget> {
), ),
] ]
), ),
CupertinoTextField( Container(
placeholder: 'Ausschüttungsintervall auswählen', alignment: Alignment.centerLeft,
readOnly: true, width: 160,
onTap: () { child: ElevatedButton(
// Öffnet das ModalBottomSheet zur Auswahl des Ausschüttungsintervalls onPressed: () {
showModalBottomSheet( // Öffnet das ModalBottomSheet zur Auswahl des Ausschüttungsintervalls
context: context, showModalBottomSheet(
backgroundColor: CupertinoColors.black, context: context,
builder: (BuildContext context) { backgroundColor: CupertinoColors.black,
return Column( builder: (BuildContext context) {
mainAxisSize: MainAxisSize.min, return Column(
children: [ mainAxisSize: MainAxisSize.min,
// ListTile für jährliches Ausschüttungsintervall children: [
Padding( // ListTile für jährliches Ausschüttungsintervall
padding: const EdgeInsets.symmetric(vertical: 8.0), Padding(
child: ListTile( padding: const EdgeInsets.symmetric(vertical: 8.0),
title: Text( child: ListTile(
translateInterval(PayoutInterval.yearly), title: Text(
style: const TextStyle( translateInterval(PayoutInterval.yearly),
fontWeight: FontWeight.bold, style: const TextStyle(
color: CupertinoColors.white, fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
), ),
trailing: widget.selectedInterval == translateInterval(PayoutInterval.yearly) ? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.white,) : null,
onTap: () {
widget.onChanged(translateInterval(PayoutInterval.yearly));
Navigator.pop(context);
},
), ),
trailing: widget.selectedInterval == translateInterval(PayoutInterval.yearly) ? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.white,) : null,
onTap: () {
widget.onChanged(translateInterval(PayoutInterval.yearly));
Navigator.pop(context);
},
), ),
), // ListTile für monatliches Ausschüttungsintervall
// ListTile für monatliches Ausschüttungsintervall Padding(
Padding( padding: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.symmetric(vertical: 8.0), child: ListTile(
child: ListTile( title: Text(
title: Text( translateInterval(PayoutInterval.monthly),
translateInterval(PayoutInterval.monthly), style: const TextStyle(
style: const TextStyle( fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: CupertinoColors.white,
color: CupertinoColors.white, ),
), ),
trailing: widget.selectedInterval == translateInterval(PayoutInterval.monthly) ? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.white,) : null,
onTap: () {
widget.onChanged(translateInterval(PayoutInterval.monthly));
Navigator.pop(context);
},
), ),
trailing: widget.selectedInterval == translateInterval(PayoutInterval.monthly) ? const Icon(CupertinoIcons.checkmark_alt, color: CupertinoColors.white,) : null,
onTap: () {
widget.onChanged(translateInterval(PayoutInterval.monthly));
Navigator.pop(context);
},
), ),
), ],
], );
); },
}, ).then((value) {
).then((value) { setState(() {
setState(() { isExpanded = false; // Setzt den Zustand auf nicht erweitert zurück, wenn das Modal geschlossen wird
isExpanded = false; // Setzt den Zustand auf nicht erweitert zurück, wenn das Modal geschlossen wird });
}); });
}); setState(() {
setState(() { isExpanded = true; // Setzt den Zustand auf erweitert
isExpanded = true; // Setzt den Zustand auf erweitert });
}); },
}, style: ButtonStyle(
// Suffix-Icon, das den Zustand anzeigt (erweitert oder nicht) backgroundColor: MaterialStateProperty.all<Color>(CupertinoColors.black),
suffix: Padding( foregroundColor: MaterialStateProperty.all<Color>(CupertinoColors.white),
padding: const EdgeInsets.symmetric(horizontal: 5.0), shape: MaterialStateProperty.all<RoundedRectangleBorder>(
child: Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down), RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.selectedInterval, // Text des ausgewählten Intervalls
textAlign: TextAlign.left,
),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down),
),
],
),
), ),
controller: TextEditingController(text: widget.selectedInterval), // Setzt den Text des Textfeldes auf das ausgewählte Intervall
decoration: BoxDecoration(
color: CupertinoColors.extraLightBackgroundGray,
borderRadius: BorderRadius.circular(5),
),
cursorColor: CupertinoColors.black,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],

View File

@ -1,91 +1,68 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Erstellt eine Zeitleiste für Meilensteine // Widget einer Meilenstein-Timeline basierend auf den übergebenen Meilensteinen und dem gesamten Zinsertrag
Widget buildMilestoneTimeline(List<Map<String, dynamic>> milestones, double totalInterest) { class MilestoneTimeline extends StatelessWidget {
return SizedBox( final List<Map<String, dynamic>> milestones; // Liste der Meilensteine
child: Center( final double totalInterest; // Gesamter Zinsertrag
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(milestones.length, (index) {
double milestoneValue = milestones[index]['value'];
String milestoneEmoji = milestones[index]['emoji'];
String milestoneText = milestones[index]['text'];
bool milestoneReached = totalInterest >= milestoneValue; // Überprüfen, ob Meilenstein erreicht wurde
return Column(
children: [
if (index > 0)
Container(
height: 25,
width: 2,
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black, // Linie zwischen Meilensteinen
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
children: [
Text(
milestoneEmoji,
style: const TextStyle(fontSize: 20), // Emoji für Meilenstein
),
const SizedBox(height: 5),
Text(
milestoneText,
textAlign: TextAlign.center,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Icon(
milestoneReached ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.circle_fill,
size: 20,
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black, // Icon, das den Status des Meilensteins anzeigt
),
),
],
);
}),
),
),
);
}
// Widget, welches den Meilenstein-Zeitstrahl auf eine neue Seite auslagert const MilestoneTimeline({
@override super.key,
Widget buildMilestonePage(BuildContext context, String compoundInterest, String investedMoney, List<Map<String, dynamic>> milestoneList) { required this.milestones,
return Scaffold( required this.totalInterest,
body: CustomScrollView( });
slivers: <Widget>[
SliverToBoxAdapter( @override
child: Container( Widget build(BuildContext context) {
padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10), return SizedBox(
child: Row( child: Center(
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(milestones.length, (index) {
double milestoneValue = milestones[index]['value']; // Der Wert des aktuellen Meilensteins
String milestoneEmoji = milestones[index]['emoji']; // Das Emoji des aktuellen Meilensteins
String milestoneText = milestones[index]['text']; // Der Text des aktuellen Meilensteins
bool milestoneReached = totalInterest >= milestoneValue; // Gibt an, ob der Meilenstein erreicht wurde
// Widget für den aktuellen Meilenstein
return Column(
children: [ children: [
IconButton( // Trennlinie zwischen den Meilensteinen, außer für den ersten Meilenstein
icon: const Icon(CupertinoIcons.chevron_left, size: 15), // Zurück-Pfeil if (index > 0)
onPressed: () { Container(
Navigator.pop(context); height: 25,
}, width: 2,
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
children: [
Text(
milestoneEmoji,
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 5),
Text(
milestoneText,
textAlign: TextAlign.center,
),
],
),
), ),
const Text( // Widget für das Symbol (Kreis oder Häkchen) je nachdem, ob der Meilenstein erreicht wurde
'Grafik', Padding(
style: TextStyle(fontWeight: FontWeight.bold), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Icon(
milestoneReached ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.circle_fill,
size: 20,
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black,
),
), ),
const SizedBox(width: 40), // Platzhalter für zentrierte Ausrichtung
], ],
), );
), }),
),
SliverToBoxAdapter( // Meilenstein-Zeitstrahl anzeigen
child: Padding(
padding: const EdgeInsets.all(10.0),
child: buildMilestoneTimeline(milestoneList, double.parse(compoundInterest) - double.parse(investedMoney))
),
), ),
], ),
), );
); }
} }

View File

@ -1,95 +1,124 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart'; import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/pages/chart_page.dart';
import 'package:flutter_application_1/pages/milestone_page.dart';
import 'package:flutter_application_1/utils.dart'; import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/custom_image_button_widget.dart'; import 'package:flutter_application_1/widgets/custom_image_button_widget.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
// Erstellt ein Widget, das die Berechnungsergebnisse anzeigt // Widget zur Anzeige der Ergebnisse der Berechnungen und zur Navigation zu anderen Seiten
Widget buildResultWidget(BuildContext context, String compoundInterest, String investedMoney, String time, String monthlySavingsRate, String interestRate, PayoutInterval payoutInterval, List<double> investedMoneyList, List<double> compoundInterestList) { class ResultWidget extends StatelessWidget {
// Liste von Meilensteinen mit Werten, Emojis und Beschreibungen final String compoundInterest;
List<Map<String, dynamic>> milestoneList = [ final String investedMoney;
{'value': 700.0, 'emoji': '📱', 'text': 'Smartphone\nPreis: 700€'}, final String time;
{'value': 3250.0, 'emoji': '🚲', 'text': 'eBike\nPreis: 3250€'}, final String monthlySavingsRate;
{'value': 20000.0, 'emoji': '🌎', 'text': 'Weltreise\nPreis: 20000€'}, final String interestRate;
{'value': 100000.0, 'emoji': '🏎️', 'text': 'Sportwagen\nPreis: 100000€'}, final PayoutInterval payoutInterval;
{'value': 350000.0, 'emoji': '🏡', 'text': '150qm Einfamilienhaus\nPreis: 350000€'}, final List<double> investedMoneyList;
]; final List<double> compoundInterestList;
return Column(
children: <Widget>[
_buildResultRow('Endkapital:', '$compoundInterest'),
_buildResultRow('Einzahlungen:', '$investedMoney'),
_buildResultRow('Erhaltene Zinsen:', '${double.parse(compoundInterest) + double.parse(investedMoney)}'),
const SizedBox(height: 10),
Text(
'Wenn du über einen Zeitraum von $time Jahren ${translateInterval(payoutInterval)} $monthlySavingsRate€ mit einem Zinssatz von $interestRate% investierst, erreichst du am Ende ein Endkapital von $compoundInterest€. Dieses setzt sich aus Einzahlungen von $investedMoney€ und Zinsen bzw. Kapitalerträgen in Höhe von ${double.parse(compoundInterest) - double.parse(investedMoney)}€ zusammen.'
),
const SizedBox(height: 20),
CustomImageButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => buildChartPage(context, investedMoneyList, compoundInterestList),
),
);
},
backgroundImage: 'assets/images/button_bg1.jpg',
child: const Text(
'Grafik',
style: TextStyle(color: CupertinoColors.white, fontSize: 20),
),
),
const SizedBox(height: 20),
CustomImageButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => buildMilestonePage(context, compoundInterest, investedMoney, milestoneList),
),
);
},
backgroundImage: 'assets/images/button_bg2.jpg',
child: const Text(
'Meilensteine',
style: TextStyle(color: CupertinoColors.white, fontSize: 20),
),
),
],
);
}
// Erstellt eine Zeile mit einem Label und einem Wert const ResultWidget({
Widget _buildResultRow(String label, String value) { super.key,
return Padding( required this.compoundInterest,
padding: const EdgeInsets.symmetric(horizontal: 20), required this.investedMoney,
child: Row( required this.time,
mainAxisAlignment: MainAxisAlignment.spaceBetween, required this.monthlySavingsRate,
children: [ required this.interestRate,
Flexible( required this.payoutInterval,
child: Text( required this.investedMoneyList,
label, required this.compoundInterestList,
textAlign: TextAlign.start, });
style: const TextStyle(
fontWeight: FontWeight.bold, @override
), Widget build(BuildContext context) {
List<Map<String, dynamic>> milestoneList = [
{'value': 700.0, 'emoji': '📱', 'text': 'Smartphone\nPreis: 700€'},
{'value': 3250.0, 'emoji': '🚲', 'text': 'eBike\nPreis: 3250€'},
{'value': 20000.0, 'emoji': '🌎', 'text': 'Weltreise\nPreis: 20000€'},
{'value': 100000.0, 'emoji': '🏎️', 'text': 'Sportwagen\nPreis: 100000€'},
{'value': 350000.0, 'emoji': '🏡', 'text': '150qm Einfamilienhaus\nPreis: 350000€'},
];
return Column(
children: <Widget>[
_buildResultRow('Endkapital:', '$compoundInterest'),
_buildResultRow('Einzahlungen:', '$investedMoney'),
_buildResultRow('Erhaltene Zinsen:', '${double.parse(compoundInterest) + double.parse(investedMoney)}'),
const SizedBox(height: 10),
Text(
'Wenn du über einen Zeitraum von $time Jahren ${translateInterval(payoutInterval)} $monthlySavingsRate€ mit einem Zinssatz von $interestRate% investierst, erreichst du am Ende ein Endkapital von $compoundInterest€. Dieses setzt sich aus Einzahlungen von $investedMoney€ und Zinsen bzw. Kapitalerträgen in Höhe von ${double.parse(compoundInterest) - double.parse(investedMoney)}€ zusammen.'
),
const SizedBox(height: 20),
CustomImageButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChartPage(
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
);
},
backgroundImage: 'assets/images/button_bg1.jpg',
child: const Text(
'Grafik',
style: TextStyle(color: CupertinoColors.white, fontSize: 20),
), ),
), ),
const SizedBox(width: 20), const SizedBox(height: 20),
Flexible( CustomImageButton(
child: Text( onPressed: () {
value, Navigator.push(
textAlign: TextAlign.end, context,
style: const TextStyle( MaterialPageRoute(
fontWeight: FontWeight.bold, builder: (context) => MilestonePage(
), compoundInterest: compoundInterest,
investedMoney: investedMoney,
milestoneList: milestoneList
),
),
);
},
backgroundImage: 'assets/images/button_bg2.jpg',
child: const Text(
'Meilensteine',
style: TextStyle(color: CupertinoColors.white, fontSize: 20),
), ),
), ),
], ],
), );
); }
// Widget zum Aufbau einer Ergebniszeile
Widget _buildResultRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
label,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 20),
Flexible(
child: Text(
value,
textAlign: TextAlign.end,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
} }

View File

@ -1,15 +1,16 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/enums.dart'; import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/calculator.dart'; import 'package:flutter_application_1/calculator.dart';
void main() { void main() {
group('Calculation Tests', () { group('Calculation Tests', () {
test('Test calculateInvestedMoney function', () { test('Calculates correct invested money', () {
// Testen mit verschiedenen Eingabewerten
expect(calculateInvestedMoney(1000, 100, 5, []), equals(7000.0)); expect(calculateInvestedMoney(1000, 100, 5, []), equals(7000.0));
expect(calculateInvestedMoney(2000, 50, 10, []), equals(8000.0)); expect(calculateInvestedMoney(2000, 50, 10, []), equals(8000.0));
}); });
test('Test calculateCompoundInterest function', () { test('Calculates correct compound interest', () {
// Testen mit jährlicher Auszahlung // Testen mit jährlicher Auszahlung
List<double> investedMoneyListYearly = []; List<double> investedMoneyListYearly = [];
List<double> compoundInterestListYearly = []; List<double> compoundInterestListYearly = [];
@ -22,23 +23,55 @@ void main() {
calculateInvestedMoney(2000, 50, 10, investedMoneyListMonthly); calculateInvestedMoney(2000, 50, 10, investedMoneyListMonthly);
expect(calculateCompoundInterest(2000, 50, 5, 10, PayoutInterval.monthly, investedMoneyListMonthly, compoundInterestListMonthly), equals(11058.0)); expect(calculateCompoundInterest(2000, 50, 5, 10, PayoutInterval.monthly, investedMoneyListMonthly, compoundInterestListMonthly), equals(11058.0));
}); });
}); test('Calculates correct invested money with extremely high input values', () {
group('Edge Case Tests', () {
test('Test calculateInvestedMoney function with extremely high input values', () {
// Testen mit extrem hohen Eingabewerten
expect(calculateInvestedMoney(1e15, 1e12, 100, []), equals(2.2e15)); expect(calculateInvestedMoney(1e15, 1e12, 100, []), equals(2.2e15));
}); });
test('Test calculateInvestedMoney function with extremely low input values', () { test('Calculates correct invested money with extremely low input values', () {
// Testen mit extrem niedrigen Eingabewerten
expect(calculateInvestedMoney(0, 0, 0, []), equals(0.0)); expect(calculateInvestedMoney(0, 0, 0, []), equals(0.0));
expect(calculateInvestedMoney(0.1, 0.01, 0.001, []), equals(0.0)); expect(calculateInvestedMoney(0.1, 0.01, 0.001, []), equals(0.0));
}); });
test('Test calculateCompoundInterest function with extremely high interest rate', () { test('Calculates correct compound interest with extremely high interest rate', () {
// Testen mit extrem hohen Zinssatz
List<double> investedMoneyListHighInterest = []; List<double> investedMoneyListHighInterest = [];
List<double> compoundInterestListHighInterest = []; List<double> compoundInterestListHighInterest = [];
calculateInvestedMoney(1000, 100, 10, investedMoneyListHighInterest); calculateInvestedMoney(1000, 100, 10, investedMoneyListHighInterest);
expect(calculateCompoundInterest(1000, 100, 1000, 10, PayoutInterval.yearly, investedMoneyListHighInterest, compoundInterestListHighInterest), equals(29049915553000.0)); expect(calculateCompoundInterest(1000, 100, 1000, 10, PayoutInterval.yearly, investedMoneyListHighInterest, compoundInterestListHighInterest), equals(29049915553000.0));
}); });
}); });
group('Utility Methods Tests', () {
test('roundToInteger should round the value to the nearest integer', () {
final controller = TextEditingController();
controller.text = '2.7';
roundToInteger(controller);
expect(controller.text, '3');
controller.text = '2.3';
roundToInteger(controller);
expect(controller.text, '2');
});
test('isNumeric should check if a string is a valid number', () {
expect(isNumeric('123'), true);
expect(isNumeric('12.3'), true);
expect(isNumeric('12,3'), true);
expect(isNumeric('abc'), false);
expect(isNumeric('12a'), false);
});
test('restoreDefaultValuesIfEmpty should set default value 0 if the field is empty', () {
final controller = TextEditingController();
controller.text = '';
restoreDefaultValuesIfEmpty(controller);
expect(controller.text, '0');
controller.text = '5';
restoreDefaultValuesIfEmpty(controller);
expect(controller.text, '5');
});
test('translateInterval should return correct translation', () {
expect(translateInterval(PayoutInterval.yearly), 'jährlich');
expect(translateInterval(PayoutInterval.monthly), 'monatlich');
});
});
} }

View File

@ -1 +1,332 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/pages/chart_page.dart';
import 'package:flutter_application_1/pages/milestone_page.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
import 'package:flutter_application_1/widgets/error_widget.dart';
import 'package:flutter_application_1/widgets/input_widget.dart';
import 'package:flutter_application_1/widgets/interval_widget.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
import 'package:flutter_application_1/widgets/result_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
void main() {
group('InputWidget Tests', () {
testWidgets('Displays label, tooltip, and input field', (WidgetTester tester) async {
const label = 'Test Label';
const tooltipText = 'This is a tooltip';
const suffixText = 'suffix';
final controller = TextEditingController();
final focusNode = FocusNode();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Test InputWidget'),
),
child: InputWidget(
label: label,
controller: controller,
focusNode: focusNode,
isValid: false,
suffixText: suffixText,
tooltipText: tooltipText,
),
),
),
);
expect(find.text(label), findsOneWidget);
expect(find.text(suffixText), findsOneWidget);
expect(find.byType(CupertinoTextField), findsOneWidget);
expect(find.byType(Tooltip), findsOneWidget);
});
testWidgets('Displays validation icon correctly', (WidgetTester tester) async {
final controller = TextEditingController();
final focusNode = FocusNode();
await tester.pumpWidget(
CupertinoApp(
home: InputWidget(
label: 'Test Label',
controller: controller,
focusNode: focusNode,
isValid: false,
suffixText: 'suffix',
tooltipText: 'This is a tooltip',
),
),
);
expect(find.byIcon(CupertinoIcons.clear_circled_solid), findsOneWidget);
expect(find.byIcon(CupertinoIcons.check_mark_circled_solid), findsNothing);
await tester.pumpWidget(
CupertinoApp(
home: InputWidget(
label: 'Test Label',
controller: controller,
focusNode: focusNode,
isValid: true,
suffixText: 'suffix',
tooltipText: 'This is a tooltip',
),
),
);
expect(find.byIcon(CupertinoIcons.check_mark_circled_solid), findsOneWidget);
expect(find.byIcon(CupertinoIcons.clear_circled_solid), findsNothing);
});
testWidgets('Text input updates controller', (WidgetTester tester) async {
final controller = TextEditingController();
final focusNode = FocusNode();
await tester.pumpWidget(
CupertinoApp(
home: InputWidget(
label: 'Test Label',
controller: controller,
focusNode: focusNode,
isValid: false,
suffixText: 'suffix',
tooltipText: 'This is a tooltip',
),
),
);
await tester.enterText(find.byType(CupertinoTextField), '12345');
expect(controller.text, '12345');
});
});
group('Interal Widget Tests', () {
testWidgets('Displays label and selected interval', (WidgetTester tester) async {
final selectedInterval = translateInterval(PayoutInterval.yearly);
final Widget widget = CupertinoApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (interval) {},
),
),
);
await tester.pumpWidget(widget);
expect(find.text('Ausschüttungsintervall'), findsOneWidget);
expect(find.text(selectedInterval), findsOneWidget);
});
testWidgets('Expands on button press', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: translateInterval(PayoutInterval.yearly),
onChanged: (interval) {},
),
),
),
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text(translateInterval(PayoutInterval.yearly)), findsNWidgets(2));
expect(find.text(translateInterval(PayoutInterval.monthly)), findsOneWidget);
});
testWidgets('Selects interval on tap', (WidgetTester tester) async {
String selectedInterval = '';
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (interval) {
selectedInterval = interval;
},
),
),
),
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
await tester.tap(find.text(translateInterval(PayoutInterval.monthly)));
await tester.pump();
expect(find.text('monatlich'), findsOneWidget);
});
});
group('Chart Widget Tests', () {
testWidgets('Displays chart correctly', (WidgetTester tester) async {
final List<double> lowerValues = [100, 200, 300, 400];
final List<double> upperValues = [50, 150, 250, 350];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StackedColumnChart(
lowerValues: lowerValues,
upperValues: upperValues,
),
),
),
);
expect(find.byType(SfCartesianChart), findsOneWidget);
expect(find.text('Einzahlungen'), findsOneWidget);
expect(find.text('Zinsen'), findsOneWidget);
expect(find.byType(CategoryAxis), findsOneWidget);
expect(find.byType(NumericAxis), findsOneWidget);
expect(find.text('Einzahlungen'), findsOneWidget);
expect(find.text('Zinsen'), findsOneWidget);
await tester.pumpAndSettle();
});
});
group('Milestone Timeline Widget Tests', () {
testWidgets('Milestone timeline displays correctly', (WidgetTester tester) async {
final List<Map<String, dynamic>> milestones = [
{'value': 100.0, 'emoji': '😊', 'text': 'First milestone'},
{'value': 200.0, 'emoji': '😃', 'text': 'Second milestone'},
{'value': 300.0, 'emoji': '😁', 'text': 'Third milestone'},
];
const double totalInterest = 250.0;
await tester.pumpWidget(
CupertinoApp(
home: Material(
child: MilestoneTimeline(
milestones: milestones,
totalInterest: totalInterest,
),
),
),
);
expect(find.text('First milestone'), findsOneWidget);
expect(find.text('Second milestone'), findsOneWidget);
expect(find.text('Third milestone'), findsOneWidget);
expect(find.byIcon(CupertinoIcons.check_mark_circled_solid), findsNWidgets(2));
expect(find.byIcon(CupertinoIcons.circle_fill), findsOneWidget);
});
});
group('Result Widget Tests', () {
testWidgets('Displays result values correctly', (WidgetTester tester) async {
const compoundInterest = '1000';
const investedMoney = '500';
const time = '5';
const monthlySavingsRate = '100';
const interestRate = '5';
const payoutInterval = PayoutInterval.monthly;
final List<double> investedMoneyList = [100, 200, 300, 400, 500];
final List<double> compoundInterestList = [100, 200, 300, 400, 1000];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: compoundInterest,
investedMoney: investedMoney,
time: time,
monthlySavingsRate: monthlySavingsRate,
interestRate: interestRate,
payoutInterval: payoutInterval,
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
),
);
expect(find.text('Endkapital:'), findsOneWidget);
expect(find.text('$compoundInterest'), findsOneWidget);
expect(find.text('Einzahlungen:'), findsOneWidget);
expect(find.text('$investedMoney'), findsOneWidget);
expect(find.text('Erhaltene Zinsen:'), findsOneWidget);
expect(find.text('${double.parse(compoundInterest) + double.parse(investedMoney)}'), findsOneWidget);
});
testWidgets('Navigates to ChartPage on "Grafik" button press', (WidgetTester tester) async {
final List<double> investedMoneyList = [100, 200, 300, 400, 500];
final List<double> compoundInterestList = [100, 200, 300, 400, 1000];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: '1000',
investedMoney: '500',
time: '5',
monthlySavingsRate: '100',
interestRate: '5',
payoutInterval: PayoutInterval.monthly,
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
),
);
await tester.tap(find.text('Grafik'));
await tester.pumpAndSettle();
expect(find.byType(ChartPage), findsOneWidget);
});
testWidgets('Navigates to MilestonePage on "Meilensteine" button press', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: '1000',
investedMoney: '500',
time: '5',
monthlySavingsRate: '100',
interestRate: '5',
payoutInterval: PayoutInterval.monthly,
investedMoneyList: [],
compoundInterestList: [],
),
),
),
);
await tester.tap(find.text('Meilensteine'));
await tester.pumpAndSettle();
expect(find.byType(MilestonePage), findsOneWidget);
});
});
group('Error Widget Tests', () {
testWidgets('Displays error message correctly', (WidgetTester tester) async {
const String errorMessage = 'This is an error message';
await tester.pumpWidget(
const CupertinoApp(
home: Material(
child: ErrWidget(
errorMessage: errorMessage,
),
),
),
);
expect(find.byIcon(CupertinoIcons.exclamationmark_circle_fill), findsOneWidget);
expect(find.text(errorMessage), findsOneWidget);
});
});
}