From a9e7380d69d1eec192957715e9a95bd4e463f71f Mon Sep 17 00:00:00 2001 From: henryhdr Date: Wed, 12 Jun 2024 12:48:30 +0200 Subject: [PATCH] Added tests, updated widget structure --- lib/enums.dart | 6 + lib/main.dart | 88 +++++- lib/pages/chart_page.dart | 179 +++++++++++ lib/pages/milestone_page.dart | 55 ++++ lib/utils.dart | 3 +- lib/widgets/chart_widget.dart | 160 +--------- lib/widgets/custom_image_button_widget.dart | 7 +- lib/widgets/error_widget.dart | 26 ++ lib/widgets/input_widget.dart | 164 +++++----- lib/widgets/interval_widget.dart | 148 +++++---- lib/widgets/milestone_timeline_widget.dart | 141 ++++----- lib/widgets/result_widget.dart | 193 +++++++----- test/unit_test.dart | 55 +++- test/widget_test.dart | 333 +++++++++++++++++++- 14 files changed, 1070 insertions(+), 488 deletions(-) create mode 100644 lib/pages/chart_page.dart create mode 100644 lib/pages/milestone_page.dart create mode 100644 lib/widgets/error_widget.dart diff --git a/lib/enums.dart b/lib/enums.dart index 731d291..8d4505f 100644 --- a/lib/enums.dart +++ b/lib/enums.dart @@ -1,4 +1,10 @@ enum PayoutInterval{ yearly, monthly +} + +enum CalculationPerformed{ + noFirstTimeItLoaded, + no, + yes } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4f6ff57..6090760 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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/interval_widget.dart'; import 'package:flutter_application_1/widgets/result_widget.dart'; +import 'package:flutter_application_1/widgets/error_widget.dart'; void main() { runApp(const MyApp()); @@ -17,7 +18,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Zinseszinsrechner', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: CupertinoColors.white, background: CupertinoColors.white), useMaterial3: true, @@ -157,7 +158,7 @@ class _MyHomePageState extends State { }); } - bool calculationPerformed = false; + CalculationPerformed _isCalculated = CalculationPerformed.noFirstTimeItLoaded; @override Widget build(BuildContext context) { @@ -170,10 +171,38 @@ class _MyHomePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildInputWidget('Anfangskapital', _initialCapitalController, _initialCapitalFocusNode, _isInitialCapitalEntered, '€', 'Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.'), - buildInputWidget('Monatliche Sparrate', _monthlySavingsRateController, _monthlySavingsRateFocusNode, _isMonthlySavingsRateEntered, '€', 'Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.'), - buildInputWidget('Jährlicher Zinssatz', _interestRateController, _interestRateFocusNode, _isInterestRateEntered, '%', 'Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.'), - buildInputWidget('Anlagezeitraum', _timeController, _timeFocusNode, _isTimeEntered, 'Jahre', 'Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.'), + InputWidget( + label: 'Anfangskapital', + controller: _initialCapitalController, + 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( selectedInterval: translateInterval(_payoutInterval), onChanged: (newInterval) { @@ -184,13 +213,29 @@ class _MyHomePageState extends State { ), ElevatedButton( onPressed: () { - setInitialCapital(); - setMonthlySavingsRate(); - setInterestRate(); - setTime(); - _investedMoney = calculateInvestedMoney(_initialCapital, _monthlySavingsRate, _time, _investedMoneyList); - _compoundInterest = calculateCompoundInterest(_initialCapital, _monthlySavingsRate, _interestRate, _time, _payoutInterval, _investedMoneyList, _compoundInterestList); - calculationPerformed = true; + if (_isInitialCapitalEntered && + _isMonthlySavingsRateEntered && + _isInterestRateEntered && + _isTimeEntered) { + setInitialCapital(); + setMonthlySavingsRate(); + 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( backgroundColor: MaterialStateProperty.all(CupertinoColors.black), @@ -209,8 +254,21 @@ class _MyHomePageState extends State { ), ), const SizedBox(height: 20), - if (calculationPerformed) - buildResultWidget(context, '$_compoundInterest', '$_investedMoney', '$_time', '$_monthlySavingsRate', '$_interestRate', _payoutInterval, _investedMoneyList, _compoundInterestList), + if(_isCalculated == CalculationPerformed.yes) + 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', + ), ], ), ) diff --git a/lib/pages/chart_page.dart b/lib/pages/chart_page.dart new file mode 100644 index 0000000..ec869e7 --- /dev/null +++ b/lib/pages/chart_page.dart @@ -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 investedMoneyList; // Liste der investierten Geldbeträge + final List 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: [ + 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, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/milestone_page.dart b/lib/pages/milestone_page.dart new file mode 100644 index 0000000..09ae7b9 --- /dev/null +++ b/lib/pages/milestone_page.dart @@ -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> 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: [ + 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)), + ), + ), + ], + ), + ); + } +} diff --git a/lib/utils.dart b/lib/utils.dart index c80c195..456bb40 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -22,6 +22,7 @@ void restoreDefaultValuesIfEmpty(TextEditingController controller) { } } +// Übersetzt das Auszahlungsintervall String translateInterval(PayoutInterval interval) { switch (interval) { case PayoutInterval.yearly: @@ -29,4 +30,4 @@ String translateInterval(PayoutInterval interval) { case PayoutInterval.monthly: return 'monatlich'; } -} \ No newline at end of file +} diff --git a/lib/widgets/chart_widget.dart b/lib/widgets/chart_widget.dart index 97f28ce..14c2564 100644 --- a/lib/widgets/chart_widget.dart +++ b/lib/widgets/chart_widget.dart @@ -1,8 +1,7 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.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 { final List lowerValues; // Liste der unteren Werte für das Diagramm final List upperValues; // Liste der oberen Werte für das Diagramm @@ -44,7 +43,6 @@ class StackedColumnChart extends StatelessWidget { color: CupertinoColors.systemGreen.highContrastColor, ), ], - // Konfiguration für die Interaktivität des Diagramms trackballBehavior: TrackballBehavior( enable: true, tooltipSettings: const InteractiveTooltip(enable: true), @@ -75,160 +73,8 @@ class StackedColumnChart extends StatelessWidget { // Klasse zur Modellierung der Datenpunkte im Diagramm class SalesData { - final int year; - final double value; + final int year; // Jahr des Datenpunkts + final double value; // Wert des Datenpunkts SalesData(this.year, this.value); } - -// Widget, welches das Diagramm auf eine neue Seite auslagert -@override -Widget buildChartPage(BuildContext context, List investedMoneyList, List compoundInterestList) { - return Scaffold( - body: CustomScrollView( - slivers: [ - 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, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ), - ); -} \ No newline at end of file diff --git a/lib/widgets/custom_image_button_widget.dart b/lib/widgets/custom_image_button_widget.dart index 852491c..fa59db5 100644 --- a/lib/widgets/custom_image_button_widget.dart +++ b/lib/widgets/custom_image_button_widget.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +// Widget für eine benutzerdefinierten Button mit einem Hintergrundbild class CustomImageButton extends StatelessWidget { - final VoidCallback onPressed; - final Widget child; - final String backgroundImage; + final VoidCallback onPressed; // Callback-Funktion für den Tastendruck + final Widget child; // Das Widget, das innerhalb der Schaltfläche angezeigt werden soll + final String backgroundImage; // Pfad zum Hintergrundbild const CustomImageButton({ super.key, diff --git a/lib/widgets/error_widget.dart b/lib/widgets/error_widget.dart new file mode 100644 index 0000000..a44e264 --- /dev/null +++ b/lib/widgets/error_widget.dart @@ -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), + ), + ], + ); + } +} diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 98f4d07..e9fc150 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -1,81 +1,103 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -// Erstellt ein Widget für die 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) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Row( - children: [ - // Label für das Eingabefeld - Text( - label, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), +// Widget zur Eingabe mit einem Label, einem Texteingabefeld, einer Validierungsanzeige und einem Tooltip +class InputWidget extends StatefulWidget { + final String label; // Beschriftung des Eingabefelds + final TextEditingController controller; // Controller für die Texteingabe + final FocusNode focusNode; // FocusNode für die Eingabe + final bool isValid; // Validierungsstatus + final String suffixText; // Suffix-Text für das Eingabefeld + final String tooltipText; // Erklärungstext im Tooltip + + const InputWidget({ + super.key, + required this.label, + required this.controller, + required this.focusNode, + required this.isValid, + required this.suffixText, + required this.tooltipText, + }); + + @override + InputWidgetState createState() => InputWidgetState(); +} + +class InputWidgetState extends State { + @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 - Tooltip( - message: tooltipText, - triggerMode: TooltipTriggerMode.tap, - decoration: const BoxDecoration( - 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), + ), + const SizedBox(width: 5), + // Tooltip mit Erklärungstext + Tooltip( + message: widget.tooltipText, + triggerMode: TooltipTriggerMode.tap, + decoration: const BoxDecoration( + 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, + ), + ), + ], + ), + // 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( - color: CupertinoColors.extraLightBackgroundGray, - borderRadius: BorderRadius.circular(5), - ), - cursorColor: CupertinoColors.black, - ), - const SizedBox(height: 20), - ], - ); + const SizedBox(height: 20), + ], + ); + } } diff --git a/lib/widgets/interval_widget.dart b/lib/widgets/interval_widget.dart index 4886e1a..ba17aa3 100644 --- a/lib/widgets/interval_widget.dart +++ b/lib/widgets/interval_widget.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_application_1/enums.dart'; import 'package:flutter_application_1/utils.dart'; +// Widget zur Auswahl des Ausschüttungsintervalls class IntervalWidget extends StatefulWidget { - final String selectedInterval; - final Function(String) onChanged; + final String selectedInterval; // Das aktuell ausgewählte Intervall + final Function(String) onChanged; // Funktion, die aufgerufen wird, wenn das Intervall geändert wird const IntervalWidget({ super.key, @@ -51,77 +52,94 @@ class IntervalWidgetState extends State { ), ] ), - CupertinoTextField( - placeholder: 'Ausschüttungsintervall auswählen', - readOnly: true, - onTap: () { - // Öffnet das ModalBottomSheet zur Auswahl des Ausschüttungsintervalls - showModalBottomSheet( - context: context, - backgroundColor: CupertinoColors.black, - builder: (BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // ListTile für jährliches Ausschüttungsintervall - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: ListTile( - title: Text( - translateInterval(PayoutInterval.yearly), - style: const TextStyle( - fontWeight: FontWeight.bold, - color: CupertinoColors.white, + Container( + alignment: Alignment.centerLeft, + width: 160, + child: ElevatedButton( + onPressed: () { + // Öffnet das ModalBottomSheet zur Auswahl des Ausschüttungsintervalls + showModalBottomSheet( + context: context, + backgroundColor: CupertinoColors.black, + builder: (BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ListTile für jährliches Ausschüttungsintervall + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + title: Text( + translateInterval(PayoutInterval.yearly), + style: const TextStyle( + 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 - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: ListTile( - title: Text( - translateInterval(PayoutInterval.monthly), - style: const TextStyle( - fontWeight: FontWeight.bold, - color: CupertinoColors.white, + // ListTile für monatliches Ausschüttungsintervall + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + title: Text( + translateInterval(PayoutInterval.monthly), + style: const TextStyle( + fontWeight: FontWeight.bold, + 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) { - setState(() { - isExpanded = false; // Setzt den Zustand auf nicht erweitert zurück, wenn das Modal geschlossen wird + ], + ); + }, + ).then((value) { + setState(() { + isExpanded = false; // Setzt den Zustand auf nicht erweitert zurück, wenn das Modal geschlossen wird + }); }); - }); - setState(() { - isExpanded = true; // Setzt den Zustand auf erweitert - }); - }, - // Suffix-Icon, das den Zustand anzeigt (erweitert oder nicht) - suffix: Padding( - padding: const EdgeInsets.symmetric(horizontal: 5.0), - child: Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down), + setState(() { + isExpanded = true; // Setzt den Zustand auf erweitert + }); + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(CupertinoColors.black), + foregroundColor: MaterialStateProperty.all(CupertinoColors.white), + shape: MaterialStateProperty.all( + 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), ], diff --git a/lib/widgets/milestone_timeline_widget.dart b/lib/widgets/milestone_timeline_widget.dart index d365a19..8992c5f 100644 --- a/lib/widgets/milestone_timeline_widget.dart +++ b/lib/widgets/milestone_timeline_widget.dart @@ -1,91 +1,68 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -// Erstellt eine Zeitleiste für Meilensteine -Widget buildMilestoneTimeline(List> milestones, double totalInterest) { - return SizedBox( - child: Center( - 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 einer Meilenstein-Timeline basierend auf den übergebenen Meilensteinen und dem gesamten Zinsertrag +class MilestoneTimeline extends StatelessWidget { + final List> milestones; // Liste der Meilensteine + final double totalInterest; // Gesamter Zinsertrag -// Widget, welches den Meilenstein-Zeitstrahl auf eine neue Seite auslagert -@override -Widget buildMilestonePage(BuildContext context, String compoundInterest, String investedMoney, List> milestoneList) { - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Container( - padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + const MilestoneTimeline({ + super.key, + required this.milestones, + required this.totalInterest, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + child: Center( + 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: [ - IconButton( - icon: const Icon(CupertinoIcons.chevron_left, size: 15), // Zurück-Pfeil - onPressed: () { - Navigator.pop(context); - }, + // Trennlinie zwischen den Meilensteinen, außer für den ersten Meilenstein + if (index > 0) + Container( + 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( - 'Grafik', - style: TextStyle(fontWeight: FontWeight.bold), + // Widget für das Symbol (Kreis oder Häkchen) je nachdem, ob der Meilenstein erreicht wurde + 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, + ), ), - 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)) - ), + ); + }), ), - ], - ), - ); + ), + ); + } } diff --git a/lib/widgets/result_widget.dart b/lib/widgets/result_widget.dart index 8243bde..2958cba 100644 --- a/lib/widgets/result_widget.dart +++ b/lib/widgets/result_widget.dart @@ -1,95 +1,124 @@ 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/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 buildResultWidget(BuildContext context, String compoundInterest, String investedMoney, String time, String monthlySavingsRate, String interestRate, PayoutInterval payoutInterval, List investedMoneyList, List compoundInterestList) { - // Liste von Meilensteinen mit Werten, Emojis und Beschreibungen - List> 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: [ - _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), - ), - ), - ], - ); - -} +// Widget zur Anzeige der Ergebnisse der Berechnungen und zur Navigation zu anderen Seiten +class ResultWidget extends StatelessWidget { + final String compoundInterest; + final String investedMoney; + final String time; + final String monthlySavingsRate; + final String interestRate; + final PayoutInterval payoutInterval; + final List investedMoneyList; + final List compoundInterestList; -// Erstellt eine Zeile mit einem Label und einem Wert -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 ResultWidget({ + super.key, + required this.compoundInterest, + required this.investedMoney, + required this.time, + required this.monthlySavingsRate, + required this.interestRate, + required this.payoutInterval, + required this.investedMoneyList, + required this.compoundInterestList, + }); + + @override + Widget build(BuildContext context) { + List> 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: [ + _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), - Flexible( - child: Text( - value, - textAlign: TextAlign.end, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + const SizedBox(height: 20), + CustomImageButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + 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, + ), + ), + ), + ], + ), + ); + } } diff --git a/test/unit_test.dart b/test/unit_test.dart index 1c2baae..ac24497 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,15 +1,16 @@ 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/calculator.dart'; void main() { group('Calculation Tests', () { - test('Test calculateInvestedMoney function', () { - // Testen mit verschiedenen Eingabewerten + test('Calculates correct invested money', () { expect(calculateInvestedMoney(1000, 100, 5, []), equals(7000.0)); expect(calculateInvestedMoney(2000, 50, 10, []), equals(8000.0)); }); - test('Test calculateCompoundInterest function', () { + test('Calculates correct compound interest', () { // Testen mit jährlicher Auszahlung List investedMoneyListYearly = []; List compoundInterestListYearly = []; @@ -22,23 +23,55 @@ void main() { calculateInvestedMoney(2000, 50, 10, investedMoneyListMonthly); expect(calculateCompoundInterest(2000, 50, 5, 10, PayoutInterval.monthly, investedMoneyListMonthly, compoundInterestListMonthly), equals(11058.0)); }); - }); - group('Edge Case Tests', () { - test('Test calculateInvestedMoney function with extremely high input values', () { - // Testen mit extrem hohen Eingabewerten + test('Calculates correct invested money with extremely high input values', () { expect(calculateInvestedMoney(1e15, 1e12, 100, []), equals(2.2e15)); }); - test('Test calculateInvestedMoney function with extremely low input values', () { - // Testen mit extrem niedrigen Eingabewerten + test('Calculates correct invested money with extremely low input values', () { expect(calculateInvestedMoney(0, 0, 0, []), equals(0.0)); expect(calculateInvestedMoney(0.1, 0.01, 0.001, []), equals(0.0)); }); - test('Test calculateCompoundInterest function with extremely high interest rate', () { - // Testen mit extrem hohen Zinssatz + test('Calculates correct compound interest with extremely high interest rate', () { List investedMoneyListHighInterest = []; List compoundInterestListHighInterest = []; calculateInvestedMoney(1000, 100, 10, investedMoneyListHighInterest); 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'); + }); + }); } diff --git a/test/widget_test.dart b/test/widget_test.dart index dbb2a48..a5249f4 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1 +1,332 @@ -import 'package:flutter_test/flutter_test.dart'; \ No newline at end of file +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 lowerValues = [100, 200, 300, 400]; + final List 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> 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 investedMoneyList = [100, 200, 300, 400, 500]; + final List 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 investedMoneyList = [100, 200, 300, 400, 500]; + final List 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); + }); + }); +} \ No newline at end of file