diff --git a/docs/implementation-flow/ch04-s01-tests.tex b/docs/implementation-flow/ch04-s01-tests.tex index 0f070f4c36..7a743581dd 100644 --- a/docs/implementation-flow/ch04-s01-tests.tex +++ b/docs/implementation-flow/ch04-s01-tests.tex @@ -498,7 +498,7 @@ \subsubsection{Writing Unit Tests with Wrappers (Code Generators)} \label{ut-cod \end{lstlisting} -\subsubsection{Adding Behavioral Tests (Gherkin)} +\subsubsection{Adding Behavioral Tests (Gherkin)} \label{t-gherkin} Improvement cycles are never ends. Previously (\ref{widget-tests}), we've discussed approach to test widgets and applied \q{When ... Given ... Then ...}-notation. That notation is a part of Behavior-Driven Development (BDD) -- the process diff --git a/docs/implementation-flow/ch04-s05-consequences.tex b/docs/implementation-flow/ch04-s05-consequences.tex index f460632bf3..519e604a9a 100644 --- a/docs/implementation-flow/ch04-s05-consequences.tex +++ b/docs/implementation-flow/ch04-s05-consequences.tex @@ -1,7 +1,7 @@ \subsection{Assessing of Ignorance} \label{ut-fail} Initially we've declared importance of having tests and ecosystem for their automation but there were not written any -valuable amount of them (10\% coverage, \ref{a-badges}). That was done to show consequences of such a decision -- not +valuable amount of them (10\% coverage, \ref{a-badges}). That was done to show a consequence of such a decision -- not to write tests. So, let's measure made mistakes during our Increment (four Iterations, two weeks each): \begin{lstlisting}[language=bash] @@ -12,7 +12,7 @@ \subsection{Assessing of Ignorance} \label{ut-fail} \end{lstlisting} \noindent \q{git log}-command retrieves a commit history (\q{\%ad} - to include the commit date, \q{--date=iso}-option -converts dates to ISO 8601 [YYY-mm-dd]; \q{\%s} - take subject) with the specified format via \q{--grep} (since we've +converts dates to ISO 8601 [YYYY-mm-dd]; \q{\%s} - take subject) with the specified format via \q{--grep} (since we've used \q{[BF]}-prefix in a title for created bug-reports [issues] and used it as a part of the commit message). \q{awk} extracts the date part from each line and delegate sorting to \q{sort}-command by the extracted dates. Finally, \q{uniq -c}-command counts the occurrences of each unique date. And the same operation we'll do for the \q{fix}-keyword. @@ -54,16 +54,16 @@ \subsection{Assessing of Ignorance} \label{ut-fail} Test-Driven Development approach is known from 1999 year as Extreme Programming flow, but for unknown to me reason not widely spread. Argumentation that "we do not have a time to write tests" is the same as "we won't use a car since -already running to reach our 200km target within a day" (instead of an hour). In Agile transformations it's a mantra +already running to reach our 200km target within a day" (instead of a few hours). In Agile transformations it's a mantra that the usage of Scrum (communication framework) will increase development flow 10 times. By looking wider, Agile, DevOps, Lean, and other approaches put an emphasis on the quality throughout the process. It's so since a communication itself has a natural limitation in the achievable performance optimization. Next 10x boost can be reached by growing exceptionally a technical excellence. Observing developers dedicating half a day to test a seemingly -"one-hour" change, I find it perplexing that the idea of investing an additional hour in crafting tests is met with +"one-hour" change, we may find it perplexing that the idea of investing an additional hour in crafting tests is met with resistance. As an example, by achieving the technical excellence through a semaphore approach ("red" - write test for the missed part of a code, assert expectations; "yellow" - write code to pass tests; "green" - refactor your code) the -stabilization phase, "monthly" regression testing, even QA Department won't be needed. All acceptance criteria for -user story, feature, and even epic are transliterated into tests, and controlled by automation. That reinforces the -developer's mental model of the code, boosts confidence and increases productivity. +stabilization phase, "monthly" regression testing, even a separate QA Department won't be needed. All acceptance +criteria for user story, feature, and even epic are transliterated into tests, and controlled by automation. That +reinforces the developer's mental model of the code, boosts confidence and increases productivity. diff --git a/docs/implementation-flow/ch05-features.tex b/docs/implementation-flow/ch05-features.tex index 8fa386ce7c..b5e46b8ceb 100644 --- a/docs/implementation-flow/ch05-features.tex +++ b/docs/implementation-flow/ch05-features.tex @@ -1,6 +1,4 @@ % Copyright 2023 The terCAD team. All rights reserved. % Use of this content is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. -\markboth{Unleashing Unparalleled Features}{Unleashing Unparalleled Features} - [TBD] \ No newline at end of file diff --git a/docs/implementation-flow/ch05-s01-tests.tex b/docs/implementation-flow/ch05-s01-tests.tex new file mode 100644 index 0000000000..78b55c562c --- /dev/null +++ b/docs/implementation-flow/ch05-s01-tests.tex @@ -0,0 +1,269 @@ +% Copyright 2023 The terCAD team. All rights reserved. +% Use of this content is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +\subsection{Benchmarking Prototype} +\markboth{Unleashing Features}{Benchmarking Prototype} + +Before adding functionality in the form of muscles to the created prototype skeleton, we need to verify its reliability. +Restructuring the fundamental concepts of the application in the future would not only pose a considerable challenge +but also entail a substantial effort and potential complications. + + +\subsubsection{Providing Integration Tests} + +Unit tests (\ref{ut-unit}) and widget tests (\ref{widget-tests}) serve as valuable tools for assessing isolated classes, +functions, or widgets. However, not all of the problems can be tackled by them. Integration tests are used to identify +systemic flaws (data corruption, concurrency problems, miscommunication between services, etc.) that might not be +evident in unit tests by verifying a synergy of individual assets, validating the application as a whole. +Integration tests are designed to reflect the real-time performance of an application on an actual device or platform. +In conclusion, they provide a vital link in the testing hierarchy by validating a collocation of various components +within an application. In such a way integration tests simulate end-to-end user workflows that we've implemented and +discussed earlier -- \ref{t-gherkin}. + +Integration tests in Flutter can be written by using \q{integration\_test}-package, \q{flutter\_driver}-package would +help us to evaluate our tests on real / virtual devices and environments and track the timeline of tests execution +(both packages are provided by the SDK): + +\begin{lstlisting}[language=yaml] +## ./pubspec.yaml +dev_dependencies: + integration_test: + sdk: flutter + flutter_driver: + sdk: flutter +\end{lstlisting} + +\noindent The implementation's deference from a widget test is in a usage of the next code line, that enables tests +execution on a physical device or platform: +\begin{lstlisting} +IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +\end{lstlisting} + + +\subsubsection{Doing Performance Testing} + +Performance testing is a type of software testing designed to evaluate the speed, responsiveness, stability, and +overall performance of an application under different conditions. It involves subjecting the application to +simulated workloads and stress scenarios to assess how it behaves in terms of speed, scalability, and resource usage. +Performance testing ensures that the software can handle the expected load without degradation in performance. + +By simulating different levels of user traffic, performance testing helps determine the application's scalability by +assessing resources utilization (CPU, memory, network bandwidth, and other parameters), and identify performance +bottlenecks, such as slow database queries, inefficient code, or network latency, and address these issues before +they will impact users. + +The detailed information about performance testing can be taken from the International Software Testing Qualifications +Board (ISTQB) or the Software Engineering Institute (SEI), while here we'll highlight only their types definition +(\cite{Ian15}, \cite{Sag16}, \cite{Sag23}): +\begin{itemize} + \item Load Testing: Evaluates how an application performs under expected load conditions. It helps determine the + application's response time, resource utilization, and overall stability. + + \item Stress Testing: Pushes the application to its limits by subjecting it to extreme conditions, such as excessive + user loads or resource scarcity. It aims to identify the breaking point and understand how the application recovers + from failures. + + \item Endurance Testing: Assesses the application's performance over an extended period to identify issues related to + memory leaks, resource exhaustion, or gradual degradation in performance. + + \item Spike Testing: Simulates sudden spikes in user traffic to assess how the application responds to rapid changes + in load. This helps uncover bottlenecks and issues related to sudden surges in demand. + + \item Volume Testing: Focuses on testing the application's performance with large volumes of data, such as a high + number of records in a database. It helps identify scalability and performance issues associated with data volume. +\end{itemize} + +\noindent Back to our process, it would be used the next command to evaluate performance tests: + +\begin{lstlisting}[language=bash] +# Precondition for Web profiling +chromedriver --port=4444 +# Launch tests +flutter drive \ + --driver=test_driver/perf_driver.dart \ + --target=test/performance/name_of_test.dart \ + --profile +\end{lstlisting} + +The \q{--profile}-option enables the application compilation in "profile mode" that helps the benchmark results to be +closer to what will be experienced by end users. By running on a mobile device or emulator it's proposed to use +\q{--no-dds}-parameter in addition, that will disable unaccessible Dart Development Service (DDS). The \q{--target} +declares the scope of test executions while \q{--driver}-option does track the outcomes. The driver configuration can be +taken from \href{https://docs.flutter.dev/cookbook/testing/integration/profiling}{https://docs.flutter.dev/cookbook/testing/integration/profiling}: + +\begin{lstlisting} +// ./test_driver/perf_driver.dart +import 'package:flutter_driver/flutter_driver.dart' as driver; +import 'package:integration_test/integration_test_driver.dart'; + +Future main() { + return integrationDriver( + responseDataCallback: (data) async { + if (data != null) { + final timeline = driver.Timeline.fromJson(data['timeline']); + final summary = driver.TimelineSummary.summarize(timeline); + await summary.writeTimelineToFile( + 'timeline', + pretty: true, + includeSummary: true, + destinationDirectory: './coverage/', + ); + } + }, + ); +} +\end{lstlisting} + +\noindent Since it's a Widget Tests'-based approach (\ref{widget-tests}, \ref{t-gherkin}), we'll accent only on the +usage of \q{traceAction}-method to store time-based metrics: + +\begin{lstlisting} +// ./test/performance/load/creation_test.dart +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets('Cover Starting Page', (WidgetTester tester) async { + await binding.traceAction(() async { + // ... other steps + final amountField = find.byWidgetPredicate((widget) { + return widget is TextField && widget.decoration?.hintText == 'Set Balance'; + }); + await tester.ensureVisible(amountField); + await tester.tap(amountField); + // In profiling mode some delay is needed: + await tester.pumpAndSettle(const Duration(seconds: 1)); + // await tester.pump(); + await tester.enterText(amountField, '1000'); + await tester.pumpAndSettle(); + expect(find.text('1000'), findsOneWidget); + // ... other steps + }, + reportKey: 'timeline', + ); + }); +} +\end{lstlisting} + +\noindent Generated file \q{timeline.timeline.json} can be traced by \q{chrome://tracing/} in Google Chrome browser +(\cref{img:perf-chrome-tracing}): + +\img{features/perf-chrome-tracing}{Google Chrome -- performance trace}{img:perf-chrome-tracing} + +\noindent The \q{timeline.timeline\_summary.json}-file can be opened in IDE as a native \q{JSON}-file and analyzed +manually a performance of the application. For example, the value of \q{average\_frame\_build\_time\_millis}-parameter +is recommended to be below 16 milliseconds to ensure that the app runs at 60 frames per second without glitches. Other +parameters are widely described on the page -- +\href{https://api.flutter.dev/flutter/flutter\_driver/TimelineSummary/summaryJson.html}{https://api.flutter.dev/flutter/flutter\_driver/TimelineSummary}. + + +\paragraph{Load Testing} +Check response time and resource utilization for the first run (Initial Setup) by creating account and budget +category: + +\begin{lstlisting}[language=cucumber] +@start +Feature: Verify Initial Flow + Scenario: Applying basic configuration through the start pages + Given I am firstly opened the app + Then I can see "Initial Setup" component + When I tap "Save to Storage (Go Next)" button + Then I can see "Acknowledge (Go Next)" component + When I tap "Acknowledge (Go Next)" button + Then I can see "Create new Account" component + When I tap on 0 index of "ListSelector" fields + And I tap "Bank Account" element + And I enter "New Account" to "Enter Account Identifier" text field + And I enter "1000" to "Set Balance" text field + And I tap "Create new Account" button + Then I can see "Create new Budget Category" component + When I enter "New Budget" to "Enter Budget Category Name" text field + And I enter "1000" to "Set Balance" text field + When I tap "Create new Budget Category" button + Then I can see "Accounts, total" component +\end{lstlisting} + +\noindent And, what we've identified from our first tests execution is a degraded \q{frame build}-parameter +(\cref{tb:frame-build}) that affects our frames per second (FPS) by generating only 37 frames instead of 60:\\ + +\begin{table}[h!] + \begin{tabular}{ |p{6.8cm}||r|r|r| } + \hline + \multicolumn{4}{|c|}{Frame Build Time, in milliseconds} \\ + \hline + Type of state & Cold Start & Retrial & With Data\\ + \hline + average & 26.00 & 24.28 & 29.65 \\ + 90th percentile & 47.20 & 43.38 & 70.33 \\ + 99th percentile & 158.31 & 159.41 & 198.03 \\ + \hline + \end{tabular} + \caption{Performance Test Results for Feature "Verify Initial Flow"} \label{tb:frame-build} +\end{table} + +\img{features/perf-slow-frame}{Performance Monitor in Visual Studio Code}{img:perf-slow-frame} + +\noindent This issue (\cref{img:perf-slow-frame}) pertains to a compilation jank in animations due to shaders +calculation (a code snippets executed on a graphics processing unit [GPU] to render a sequence of draw commands). +Their pre-compilation strategy mitigates the compilation-related disruptions during subsequent animations, and improves +frames per second rendering. To run the app with \q{--cache-sksl} turned on to capture shaders in SkSL: + +\begin{lstlisting}[language=bash] +flutter run --profile --cache-sksl --purge-persistent-cache +\end{lstlisting} + +\noindent Warm-up shaders in Skia Shader Language (SkSL) format for an application build: + +\begin{lstlisting}[language=bash] +# Capture shaders in Skia Shader Language (SkSL) format into a file +flutter drive --profile --cache-sksl --write-sksl-on-exit sksl.json -t test_driver/warm_up.dart +# Build app with SkSL warm-up +flutter build ios --bundle-sksl-path sksl.json +\end{lstlisting} + +\begin{lstlisting} +// ./test_driver/warm_up.dart +import 'package:integration_test/integration_test_driver.dart'; +Future main() { + return integrationDriver();(*@ \stopnumber @*) +} + +// ./test_driver/warm_up_test.dart +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferencesMixin.pref = await SharedPreferences.getInstance(); + + testWidgets('Warm-up', (WidgetTester tester) async { + await tester.pumpWidget(MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => AppData(), + ), + ChangeNotifierProvider( + create: (_) => AppTheme(ThemeMode.system), + ), + ], + child: const MyApp(), + )); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); +} +\end{lstlisting} + +\noindent Finally, we've taken \q{56 FPS (average)} as an outcome from that tunning. + + +\paragraph{Stress Testing} +Check initial load (a time before the enabled interaction) with a huge transaction log history (32Mb, 128Mb, +512Mb, 2Gb). + + +\paragraph{Endurance Testing} +Check response time and resource utilization by adding different types of data within a different time +periods (15 minutes, an hour, 4 hours, 16 hours). + + +\paragraph{Spike Testing} +Postponed till the enabled synchronization between different devices. + + +\paragraph{Volume Testing} +Combine reporting of "Load Testing" with data from "Stress Testing". diff --git a/docs/implementation-flow/features/perf-chrome-tracing.png b/docs/implementation-flow/features/perf-chrome-tracing.png new file mode 100644 index 0000000000..e23112fd29 Binary files /dev/null and b/docs/implementation-flow/features/perf-chrome-tracing.png differ diff --git a/docs/implementation-flow/features/perf-slow-frame.png b/docs/implementation-flow/features/perf-slow-frame.png new file mode 100644 index 0000000000..e1a3e5f23c Binary files /dev/null and b/docs/implementation-flow/features/perf-slow-frame.png differ diff --git a/docs/implementation-flow/index.tex b/docs/implementation-flow/index.tex index 0fa26eecee..18a6c25ba7 100644 --- a/docs/implementation-flow/index.tex +++ b/docs/implementation-flow/index.tex @@ -39,6 +39,7 @@ \crefname{table}{Table}{Tables} \usepackage{multicol} \usepackage{pgfplots} +\usepackage{tabularx} \usepackage{_lib/customization} \usepackage{_lib/code-style} @@ -114,20 +115,21 @@ \section{[WIP] Implementing Core Functionality} \include{./ch03-s02-subscription} \newpage -\section{[WIP] Defining Quality Gates} +\section{Defining Quality Gates} \input{./ch04-quality-gates} \input{./ch04-s01-tests} -\include{./ch04-s02-automation} +\input{./ch04-s02-automation} \input{./ch04-s03-telemetry} -\include{./ch04-s04-deployment} -\include{./ch04-s05-consequences} +\input{./ch04-s04-deployment} +\input{./ch04-s05-consequences} \newpage -\section{[TBD] Unleashing Features} +\section{[WIP] Unleashing Features} \input{./ch05-features} +\input{./ch05-s01-tests} \newpage -\section{[WIP] Optimizing UI/UX Flow} +\section{[TBD] Optimizing UI/UX Flow} \input{./ch06-s01-autofocus} \newpage diff --git a/docs/implementation-flow/references.tex b/docs/implementation-flow/references.tex index 9e93fc0e41..d6d8a5efc6 100644 --- a/docs/implementation-flow/references.tex +++ b/docs/implementation-flow/references.tex @@ -29,7 +29,15 @@ architecture and assessment", \emph{Packt Publishing}, ISBN 9781788299237, p. 230, May 2017 \bibitem[Suz12]{Suz12} Suzanne Robertson, James Robertson, ``Mastering the Requirements Process: Getting Requirements -Right", \emph{Addison-Wesley Professional}, ISBN 978-0321815743, August 2012 +Right", \emph{Addison-Wesley Professional}, ISBN 9780321815743, August 2012 +\bibitem[Ian15]{Ian15} Ian Molyneaux, ``The Art of Application Performance Testing: From Strategy to Tools", +\emph{O'Reilly Media}, ISBN 9781491900543, p. 275, January 2015 + +\bibitem[Sag23]{Sag23} Sagar Deshpande, Sagar Tambade, ``Performance Testing Unleashed: A Journey from Novice to Expert", +\emph{Independently published}, ISBN 9798398536317, p. 102, June 2023 + +\bibitem[Sag16]{Sag16} Sagar Deshpande, Ravindra Sadaphule, ``Demystifying Scalability", \emph{CreateSpace Independent +Publishing Platform}, ISBN 9781533040510, p. 62, April 2016 \end{thebibliography} diff --git a/integration_test/README.md b/integration_test/README.md new file mode 100644 index 0000000000..a1b39dd39e --- /dev/null +++ b/integration_test/README.md @@ -0,0 +1,10 @@ +## Tips to evaluate Integration Tests + +``` +flutter drive \ + --driver=test_driver/perf_driver.dart \ + --target=integration_test/{name}_test.dart \ + --no-dds +``` + +P.S. Launch Chrome Driver `chromedriver --port=4444` for Web profiling diff --git a/integration_test/load/start_page_test.dart b/integration_test/load/start_page_test.dart new file mode 100644 index 0000000000..a9c6038870 --- /dev/null +++ b/integration_test/load/start_page_test.dart @@ -0,0 +1,70 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/app_data.dart'; +import 'package:app_finance/_classes/app_theme.dart'; +import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; +import 'package:app_finance/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../test/e2e/_steps/file_reader.dart'; +import '../../test/e2e/_steps/file_reporter.dart'; +import '../../test/e2e/_steps/file_runner.dart'; + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + _init(WidgetTester tester) async { + SharedPreferencesMixin.pref = await SharedPreferences.getInstance(); + await tester.pumpWidget(MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => AppData(), + ), + ChangeNotifierProvider( + create: (_) => AppTheme(ThemeMode.system), + ), + ], + child: const MyApp(), + )); + } + + testWidgets('Cover Starting Page', (WidgetTester tester) async { + await binding.traceAction( + () async { + await _init(tester); + final reporter = FileReporter(); + final step = await FileReader().getFromString(''' + @start + Feature: Verify Initial Flow + Scenario: Applying basic configuration through the start pages + Given I am firstly opened the app + Then I can see "Initial Setup" component + When I tap "Save to Storage (Go Next)" button + Then I can see "Acknowledge (Go Next)" component + When I tap "Acknowledge (Go Next)" button + Then I can see "Create new Account" component + When I tap on 0 index of "ListSelector" fields + And I tap "Bank Account" element + And I enter "Starting Page Account" to "Enter Account Identifier" text field + And I enter "1000" to "Set Balance" text field + And I tap "Create new Account" button + Then I can see "Create new Budget Category" component + When I enter "Starting Page Budget" to "Enter Budget Category Name" text field + And I enter "1000" to "Set Balance" text field + When I tap "Create new Budget Category" button + Then I can see "Accounts, total" component + ''', reporter); + final runner = FileRunner(tester, reporter); + final result = await runner.run(step); + reporter.publish(); + expectSync(result, true); + }, + reportKey: 'timeline', + ); + }); +} diff --git a/lib/_classes/app_data.dart b/lib/_classes/app_data.dart index 1e819f2f2a..136b2cb3dc 100644 --- a/lib/_classes/app_data.dart +++ b/lib/_classes/app_data.dart @@ -27,27 +27,9 @@ enum AppDataType { currencies, } -enum AppAccountType { - account, - cash, - debitCard, - creditCard, - deposit, - credit, -} - class AppData extends ChangeNotifier { bool isLoading = false; - final _account = { - AppAccountType.account: (), - AppAccountType.cash: (), - AppAccountType.debitCard: (), - AppAccountType.creditCard: (), - AppAccountType.deposit: (), - AppAccountType.credit: (), - }; - final _hashTable = HashMap(); final _history = HashMap>(); @@ -247,10 +229,6 @@ class AppData extends ChangeNotifier { return obj; } - dynamic getType(AppAccountType property) { - return _account[property]; - } - List? getLog(String uuid) { return _history[uuid]?.reversed.toList(); } diff --git a/lib/_classes/data/account_type.dart b/lib/_classes/data/account_type.dart new file mode 100644 index 0000000000..29a56dd4ff --- /dev/null +++ b/lib/_classes/data/account_type.dart @@ -0,0 +1,32 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/widgets/_forms/list_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localization.dart'; + +enum AppAccountType { + account, + cash, + debitCard, + creditCard, + deposit, + credit, +} + +class AccountType { + final BuildContext context; + + AccountType(this.context); + + List toList() { + return [ + ListSelectorItem(id: AppAccountType.account.toString(), name: AppLocalizations.of(context)!.bankAccount), + ListSelectorItem(id: AppAccountType.cash.toString(), name: AppLocalizations.of(context)!.cash), + ListSelectorItem(id: AppAccountType.debitCard.toString(), name: AppLocalizations.of(context)!.debitCard), + ListSelectorItem(id: AppAccountType.creditCard.toString(), name: AppLocalizations.of(context)!.creditCard), + ListSelectorItem(id: AppAccountType.deposit.toString(), name: AppLocalizations.of(context)!.deposit), + ListSelectorItem(id: AppAccountType.credit.toString(), name: AppLocalizations.of(context)!.credit), + ]; + } +} diff --git a/lib/_classes/focus_controller.dart b/lib/_classes/focus_controller.dart index d7491d7c99..b9c6227aa7 100644 --- a/lib/_classes/focus_controller.dart +++ b/lib/_classes/focus_controller.dart @@ -1,6 +1,5 @@ // Copyright 2023 The terCAD team. All rights reserved. -// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be -// found in the LICENSE file. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. import 'package:app_finance/_classes/delayed_call.dart'; import 'package:flutter/material.dart'; @@ -39,7 +38,7 @@ class FocusController { } static bool isLast() { - return _idx >= nodes.length; + return focus + 1 == nodes.length; } static TextInputAction getAction() { @@ -85,7 +84,7 @@ class FocusController { } static void onEditingComplete(index) { - resetFocus(); + focus = DEFAULT; if (index >= 0) { values.fillRange(0, index + 1, true); } @@ -101,10 +100,6 @@ class FocusController { requestFocus(); } - static void resetFocus() { - focus = DEFAULT; - } - static bool isFocused(int idx, dynamic value) { bool isFocused = false; if ((value == null || value == '') && idx != DEFAULT && (focus == DEFAULT || focus == idx)) { @@ -121,6 +116,7 @@ class FocusController { nodes.remove(node); } values.removeWhere((value) => true); - resetFocus(); + focus = DEFAULT; + _idx = DEFAULT; } } diff --git a/lib/_classes/gen/wrapper_generator.dart b/lib/_classes/gen/wrapper_generator.dart index 7e0f50b0ef..ea6392caee 100644 --- a/lib/_classes/gen/wrapper_generator.dart +++ b/lib/_classes/gen/wrapper_generator.dart @@ -28,13 +28,13 @@ class WrapperGenerator extends Generator { } Set imports = {}; for (final name in classes.toListValue()!) { - imports.addAll(WrapperVisitor.getImports(name.toTypeValue()?.element as ClassElement)); + imports.addAll(WrapperVisitor.getImports(name.toTypeValue()?.element as InterfaceElement)); } result.writeln(imports.toList().join('\n')); result.writeln(''); for (final name in classes.toListValue()!) { final type = name.toTypeValue(); - final classElement = type?.element as ClassElement; + final classElement = type?.element as InterfaceElement; final visitor = WrapperVisitor(classElement); result.writeln(visitor.toString()); } diff --git a/lib/_classes/gen/wrapper_visitor.dart b/lib/_classes/gen/wrapper_visitor.dart index 89352c9f4a..ba23196848 100644 --- a/lib/_classes/gen/wrapper_visitor.dart +++ b/lib/_classes/gen/wrapper_visitor.dart @@ -7,12 +7,12 @@ import 'package:analyzer/dart/element/element.dart'; class WrapperVisitor { StringBuffer buffer = StringBuffer(); - ClassElement element; + InterfaceElement element; String? singleton; WrapperVisitor(this.element); - static List getImports(ClassElement element) { + static List getImports(InterfaceElement element) { final mainClass = element.enclosingElement.library; List result = [_getImport(mainClass)]; for (final cls in mainClass.importedLibraries) { @@ -30,16 +30,20 @@ class WrapperVisitor { return "${ignore}import '$path';"; } - void addClassDefinition() { - final constructor = element.unnamedConstructor; - if (constructor == null) { - buffer.writeln('class Wrapper${element.name} implements ${element.name} {'); - buffer.writeln(' static ${element.name}? wrap${element.name};'); - singleton = element.name; - return; - } + void _withMixin() { + buffer.writeln('class Wrapper${element.name} with ${element.name} {'); + } + + void _withPrivateConstructor() { + buffer.writeln('class Wrapper${element.name} implements ${element.name} {'); + buffer.writeln(' static ${element.name}? wrap${element.name};'); + singleton = element.name; + } + + void _withConstructor() { buffer.writeln('class Wrapper${element.name} extends ${element.name} {'); - if (!constructor.isDefaultConstructor) { + final constructor = element.unnamedConstructor; + if (!constructor!.isDefaultConstructor) { final properties = constructor.parameters; buffer.writeln(' Wrapper${element.name}({'); if (properties.isNotEmpty) { @@ -51,6 +55,16 @@ class WrapperVisitor { } } + void addClassDefinition() { + if (element is MixinElement) { + return _withMixin(); + } + if (element.unnamedConstructor == null) { + return _withPrivateConstructor(); + } + _withConstructor(); + } + String _getTypedArguments(List parameters) { List optional = []; List named = []; @@ -90,7 +104,7 @@ class WrapperVisitor { buffer.writeln(' ${static}set ${_getName(m)}(${m.returnType} value) {'); buffer.writeln(' _${m.name} = value;'); buffer.writeln(' }'); - if (singleton == null) { + if (singleton == null && !m.isStatic) { buffer.writeln(' @override'); buffer.writeln(' // ignore: unnecessary_overrides'); } @@ -119,7 +133,9 @@ class WrapperVisitor { buffer.writeln(' $static$m => _${m.name} != null ? ' '_${m.name}!($args) : ${m.isStatic ? singleton : 'wrap$singleton!'}.${m.name}($args);'); } else { - buffer.writeln(' @override'); + if (!m.isStatic) { + buffer.writeln(' @override'); + } buffer.writeln(' $static$m => (_${m.name} ?? ${m.isStatic ? element.name : 'super'}.${m.name})($args);'); } } diff --git a/lib/_mixins/formatter_mixin.dart b/lib/_mixins/formatter_mixin.dart index 4282f77155..27cbbc45c4 100644 --- a/lib/_mixins/formatter_mixin.dart +++ b/lib/_mixins/formatter_mixin.dart @@ -11,25 +11,27 @@ mixin FormatterMixin { BuildContext? _context; Currency? currency; - dynamic updateContext(BuildContext context) { + dynamic setContext(BuildContext context) { _context = context; return this; } BuildContext? getContext() => _context; + String? getLocale() { + return Localizations.localeOf(_context!).toString(); + } + String getDateFormatted(DateTime date) { - final locale = Localizations.localeOf(_context!).toString(); - final DateFormat formatterDate = DateFormat.yMEd(locale); + final DateFormat formatterDate = DateFormat.yMEd(getLocale()); return formatterDate.format(date); } String getNumberFormatted(double value) { - final locale = Localizations.localeOf(_context!).toString(); final NumberFormat formatter = NumberFormat.currency( - locale: locale, + locale: getLocale(), symbol: currency?.symbol ?? '?', - decimalDigits: 2, + decimalDigits: currency?.decimalDigits ?? 2, ); return formatter.format(value); } diff --git a/lib/routes/account_add_page.dart b/lib/routes/account_add_page.dart index d841ad167f..3dfb82d2d7 100644 --- a/lib/routes/account_add_page.dart +++ b/lib/routes/account_add_page.dart @@ -5,6 +5,7 @@ import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; import 'package:app_finance/_classes/currency/currency_provider.dart'; import 'package:app_finance/_classes/data/account_app_data.dart'; +import 'package:app_finance/_classes/data/account_type.dart'; import 'package:app_finance/_classes/focus_controller.dart'; import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; import 'package:app_finance/custom_text_theme.dart'; @@ -14,6 +15,7 @@ import 'package:app_finance/routes/abstract_page.dart'; import 'package:app_finance/widgets/_forms/color_selector.dart'; import 'package:app_finance/widgets/_forms/currency_selector.dart'; import 'package:app_finance/widgets/_forms/date_time_input.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:app_finance/widgets/_forms/icon_selector.dart'; import 'package:app_finance/widgets/_forms/list_selector.dart'; import 'package:app_finance/widgets/_forms/month_year_input.dart'; @@ -121,41 +123,14 @@ class AccountAddPageState extends AbstractPageState triggerActionButton(context), - focusNode: FocusController.getFocusNode(), - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: () => triggerActionButton(context), + title: getButtonName(), + icon: Icons.save, ); } - List getAccountTypes(BuildContext context) { - return [ - ListSelectorItem(id: AppAccountType.account.toString(), name: AppLocalizations.of(context)!.bankAccount), - ListSelectorItem(id: AppAccountType.cash.toString(), name: AppLocalizations.of(context)!.cash), - ListSelectorItem(id: AppAccountType.debitCard.toString(), name: AppLocalizations.of(context)!.debitCard), - ListSelectorItem(id: AppAccountType.creditCard.toString(), name: AppLocalizations.of(context)!.creditCard), - ListSelectorItem(id: AppAccountType.deposit.toString(), name: AppLocalizations.of(context)!.deposit), - ListSelectorItem(id: AppAccountType.credit.toString(), name: AppLocalizations.of(context)!.credit), - ]; - } - @override Widget buildContent(BuildContext context, BoxConstraints constraints) { final TextTheme textTheme = Theme.of(context).textTheme; @@ -176,7 +151,7 @@ class AccountAddPageState extends AbstractPageState setState(() => type = value), style: textTheme.numberMedium.copyWith(color: textTheme.headlineSmall?.color), indent: indent, diff --git a/lib/routes/account_edit_page.dart b/lib/routes/account_edit_page.dart index fe7d2489bf..0000fff0d0 100644 --- a/lib/routes/account_edit_page.dart +++ b/lib/routes/account_edit_page.dart @@ -4,6 +4,7 @@ import 'package:app_finance/_classes/data/account_app_data.dart'; import 'package:app_finance/_classes/app_data.dart'; +import 'package:app_finance/_classes/data/account_type.dart'; import 'package:app_finance/routes/account_add_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localization.dart'; diff --git a/lib/routes/account_view_page.dart b/lib/routes/account_view_page.dart index 985937b008..9cdaef1e4b 100644 --- a/lib/routes/account_view_page.dart +++ b/lib/routes/account_view_page.dart @@ -61,7 +61,7 @@ class AccountViewPageState extends AbstractPageState { } Widget buildListWidget(item, BuildContext context, double offset) { - item.updateContext(context); + item.setContext(context); return BaseLineWidget( uuid: '', title: '', @@ -76,7 +76,7 @@ class AccountViewPageState extends AbstractPageState { @override Widget buildContent(BuildContext context, BoxConstraints constraints) { final item = super.state.getByUuid(widget.uuid) as AccountAppData; - item.updateContext(context); + item.setContext(context); double indent = ThemeHelper(windowType: getWindowType(context)).getIndent() * 2; double offset = MediaQuery.of(context).size.width - indent * 3; return Column( diff --git a/lib/routes/bill_edit_page.dart b/lib/routes/bill_edit_page.dart index c000089c82..70a9293864 100644 --- a/lib/routes/bill_edit_page.dart +++ b/lib/routes/bill_edit_page.dart @@ -40,7 +40,7 @@ class BillEditPageState extends AbstractPageState { @override Widget buildContent(BuildContext context, BoxConstraints constraints) { final item = super.state.getByUuid(widget.uuid) as BillAppData; - item.updateContext(context); + item.setContext(context); double indent = ThemeHelper(windowType: getWindowType(context)).getIndent() * 2; double offset = MediaQuery.of(context).size.width - indent * 3; return Column( diff --git a/lib/routes/budget_add_page.dart b/lib/routes/budget_add_page.dart index 934099f5de..a791edbf03 100644 --- a/lib/routes/budget_add_page.dart +++ b/lib/routes/budget_add_page.dart @@ -13,6 +13,7 @@ import 'package:app_finance/helpers/theme_helper.dart'; import 'package:app_finance/routes/abstract_page.dart'; import 'package:app_finance/widgets/_forms/color_selector.dart'; import 'package:app_finance/widgets/_forms/currency_selector.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:app_finance/widgets/_forms/icon_selector.dart'; import 'package:app_finance/widgets/_forms/simple_input.dart'; import 'package:app_finance/widgets/_wrappers/required_widget.dart'; @@ -101,26 +102,11 @@ class BudgetAddPageState extends AbstractPageState triggerActionButton(context), - focusNode: FocusController.getFocusNode(), - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: () => triggerActionButton(context), + title: getButtonName(), + icon: Icons.save, ); } diff --git a/lib/routes/budget_view_page.dart b/lib/routes/budget_view_page.dart index a0658bb55d..bbbff9c5c0 100644 --- a/lib/routes/budget_view_page.dart +++ b/lib/routes/budget_view_page.dart @@ -61,7 +61,7 @@ class BudgetViewPageState extends AbstractPageState { } Widget buildListWidget(item, BuildContext context, double offset) { - item.updateContext(context); + item.setContext(context); return BaseLineWidget( uuid: '', title: '', @@ -76,7 +76,7 @@ class BudgetViewPageState extends AbstractPageState { @override Widget buildContent(BuildContext context, BoxConstraints constraints) { final item = super.state.getByUuid(widget.uuid) as BudgetAppData; - item.updateContext(context); + item.setContext(context); double indent = ThemeHelper(windowType: getWindowType(context)).getIndent() * 2; double offset = MediaQuery.of(context).size.width - indent * 3; return Column( diff --git a/lib/routes/currency_page.dart b/lib/routes/currency_page.dart index cf852ba9fb..39fb7c5d64 100644 --- a/lib/routes/currency_page.dart +++ b/lib/routes/currency_page.dart @@ -63,7 +63,7 @@ class CurrencyPageState extends AbstractPageState { itemCount: scope?.length, itemBuilder: (context, index) { final item = scope![index]; - item.updateContext(context); + item.setContext(context); return Padding( padding: EdgeInsets.all(indent), child: RowWidget( diff --git a/lib/routes/goal_add_page.dart b/lib/routes/goal_add_page.dart index 6e3aee9060..31d3d08bb3 100644 --- a/lib/routes/goal_add_page.dart +++ b/lib/routes/goal_add_page.dart @@ -13,6 +13,7 @@ import 'package:app_finance/routes/abstract_page.dart'; import 'package:app_finance/widgets/_forms/color_selector.dart'; import 'package:app_finance/widgets/_forms/currency_selector.dart'; import 'package:app_finance/widgets/_forms/date_input.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:app_finance/widgets/_forms/icon_selector.dart'; import 'package:app_finance/widgets/_forms/simple_input.dart'; import 'package:app_finance/widgets/_wrappers/required_widget.dart'; @@ -94,35 +95,20 @@ class GoalAddPageState extends AbstractPageState { - setState(() { - if (hasFormErrors()) { - return; - } - updateStorage(); - Navigator.pop(context); - Navigator.pop(context); - }) - }, - focusNode: FocusController.getFocusNode(), - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: () => { + setState(() { + if (hasFormErrors()) { + return; + } + updateStorage(); + Navigator.pop(context); + Navigator.pop(context); + }) + }, + title: getButtonName(), + icon: Icons.save, ); } diff --git a/lib/routes/goal_page.dart b/lib/routes/goal_page.dart index ba9863f660..11e8dfeb62 100644 --- a/lib/routes/goal_page.dart +++ b/lib/routes/goal_page.dart @@ -39,7 +39,7 @@ class GoalPageState extends AbstractPageState { final double offset = MediaQuery.of(context).size.width - helper.getIndent() * 2; return Column( children: super.state.getList(AppDataType.goals).map((goal) { - goal.updateContext(context); + goal.setContext(context); return BaseLineWidget( title: goal.title ?? '', offset: offset, diff --git a/lib/routes/goal_view_page.dart b/lib/routes/goal_view_page.dart index a125c99a69..67d5417eab 100644 --- a/lib/routes/goal_view_page.dart +++ b/lib/routes/goal_view_page.dart @@ -91,7 +91,7 @@ class GoalViewPageState extends AbstractPageState with SharedPrefe @override Widget buildContent(BuildContext context, BoxConstraints constraints) { final item = super.state.getByUuid(widget.uuid) as GoalAppData; - item.updateContext(context); + item.setContext(context); double indent = ThemeHelper(windowType: getWindowType(context)).getIndent() * 2; double offset = MediaQuery.of(context).size.width - indent * 3; return Column( diff --git a/lib/routes/home_page.dart b/lib/routes/home_page.dart index 979f1f26a8..76bb39c375 100644 --- a/lib/routes/home_page.dart +++ b/lib/routes/home_page.dart @@ -35,7 +35,7 @@ class HomePageState extends AbstractPageState with SharedPreferencesMi super.initState(); toExpand = getPreference(prefExpand); if (getPreference(prefPrivacyPolicy) == null) { - Future.delayed(Duration.zero, () => Navigator.popAndPushNamed(context, AppRoute.startRoute)); + WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.popAndPushNamed(context, AppRoute.startRoute)); } } @@ -64,15 +64,7 @@ class HomePageState extends AbstractPageState with SharedPreferencesMi }, ), title: Center( - child: Image.asset( - 'assets/images/fingram.png', - // width: width * 0.2, - // height: width * 0.2, - ), - /*child: Text( - getTitle(context), - style: TextStyle(color: Theme.of(context).colorScheme.inversePrimary), - ),*/ + child: Image.asset('assets/images/fingram.png'), ), actions: [ ToolbarButtonWidget( diff --git a/lib/widgets/_forms/full_sized_button.dart b/lib/widgets/_forms/full_sized_button.dart new file mode 100644 index 0000000000..2987139938 --- /dev/null +++ b/lib/widgets/_forms/full_sized_button.dart @@ -0,0 +1,57 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; +import 'package:app_finance/_classes/focus_controller.dart'; +import 'package:app_finance/helpers/theme_helper.dart'; +import 'package:app_finance/widgets/_forms/abstract_input.dart'; +import 'package:flutter/material.dart'; + +typedef OnPressedFunction = void Function(); + +class FullSizedButton extends AbstractInput { + final OnPressedFunction setState; + final String title; + final IconData? icon; + final BoxConstraints constraints; + + FullSizedButton({ + super.key, + required this.setState, + required this.constraints, + required this.title, + this.icon, + }) : super(value: null); + + @override + Widget build(BuildContext context) { + final helper = ThemeHelper(windowType: getWindowType(context)); + return SizedBox( + width: constraints.maxWidth - helper.getIndent() * 4, + child: FloatingActionButton( + onPressed: setState, + tooltip: title, + focusNode: FocusController.getFocusNode(), + child: Row( + children: [ + if (icon != null) + Icon( + icon, + semanticLabel: title, + size: 32, + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + ), + Expanded( + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/_forms/list_account_selector.dart b/lib/widgets/_forms/list_account_selector.dart index 0b776ee332..7ef278fec1 100644 --- a/lib/widgets/_forms/list_account_selector.dart +++ b/lib/widgets/_forms/list_account_selector.dart @@ -58,7 +58,7 @@ class ListAccountSelector extends ListSelecto @override Widget selectorBuilder(context, item, [bool showDivider = false]) { - item.item.updateContext(context); + item.item.setContext(context); return BaseLineWidget( uuid: item.item?.uuid ?? '', title: item.item?.title ?? '', diff --git a/lib/widgets/bill/expenses_tab.dart b/lib/widgets/bill/expenses_tab.dart index ea61904986..bb9a0bda1c 100644 --- a/lib/widgets/bill/expenses_tab.dart +++ b/lib/widgets/bill/expenses_tab.dart @@ -14,6 +14,7 @@ import 'package:app_finance/helpers/theme_helper.dart'; import 'package:app_finance/widgets/_forms/currency_exchange_input.dart'; import 'package:app_finance/widgets/_forms/currency_selector.dart'; import 'package:app_finance/widgets/_forms/date_time_input.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:app_finance/widgets/_forms/list_account_selector.dart'; import 'package:app_finance/widgets/_forms/list_budget_selector.dart'; import 'package:app_finance/widgets/_forms/simple_input.dart'; @@ -115,34 +116,19 @@ class ExpensesTabState extends State with SharedPrefer } Widget buildButton(BuildContext context, BoxConstraints constraints) { - var helper = ThemeHelper(windowType: getWindowType(context)); - String title = getButtonTitle(context); - return SizedBox( - width: constraints.maxWidth - helper.getIndent() * 4, - child: FloatingActionButton( - onPressed: () => { - setState(() { - if (hasFormErrors()) { - return; - } - updateStorage(); - Navigator.popAndPushNamed(context, AppRoute.homeRoute); - }) - }, - focusNode: FocusController.getFocusNode(), - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: () => { + setState(() { + if (hasFormErrors()) { + return; + } + updateStorage(); + Navigator.popAndPushNamed(context, AppRoute.homeRoute); + }) + }, + title: getButtonTitle(context), + icon: Icons.save, ); } diff --git a/lib/widgets/bill/income_tab.dart b/lib/widgets/bill/income_tab.dart index 5c5eaf026f..f8b1b1bdea 100644 --- a/lib/widgets/bill/income_tab.dart +++ b/lib/widgets/bill/income_tab.dart @@ -13,6 +13,7 @@ import 'package:app_finance/_classes/app_data.dart'; import 'package:app_finance/helpers/theme_helper.dart'; import 'package:app_finance/widgets/_forms/currency_exchange_input.dart'; import 'package:app_finance/widgets/_forms/currency_selector.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:app_finance/widgets/_forms/list_account_selector.dart'; import 'package:app_finance/widgets/_forms/simple_input.dart'; import 'package:app_finance/widgets/_wrappers/required_widget.dart'; @@ -80,34 +81,19 @@ class IncomeTabState extends State with SharedPreferencesMixin { } Widget buildButton(BuildContext context, BoxConstraints constraints) { - var helper = ThemeHelper(windowType: getWindowType(context)); - String title = AppLocalizations.of(context)!.createIncomeTooltip; - return SizedBox( - width: constraints.maxWidth - helper.getIndent() * 4, - child: FloatingActionButton( - onPressed: () => { - setState(() { - if (hasFormErrors()) { - return; - } - updateStorage(); - Navigator.popAndPushNamed(context, AppRoute.homeRoute); - }) - }, - focusNode: FocusController.getFocusNode(), - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: () => { + setState(() { + if (hasFormErrors()) { + return; + } + updateStorage(); + Navigator.popAndPushNamed(context, AppRoute.homeRoute); + }) + }, + title: AppLocalizations.of(context)!.createIncomeTooltip, + icon: Icons.save, ); } diff --git a/lib/widgets/bill/transfer_tab.dart b/lib/widgets/bill/transfer_tab.dart index 70654d878d..ec511abd9c 100644 --- a/lib/widgets/bill/transfer_tab.dart +++ b/lib/widgets/bill/transfer_tab.dart @@ -11,6 +11,7 @@ import 'package:app_finance/custom_text_theme.dart'; import 'package:app_finance/_classes/app_data.dart'; import 'package:app_finance/helpers/theme_helper.dart'; import 'package:app_finance/widgets/_forms/currency_exchange_input.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:app_finance/widgets/_wrappers/required_widget.dart'; import 'package:app_finance/widgets/_wrappers/row_widget.dart'; import 'package:app_finance/widgets/_forms/currency_selector.dart'; @@ -76,34 +77,19 @@ class TransferTabState extends State { } Widget buildButton(BuildContext context, BoxConstraints constraints) { - var helper = ThemeHelper(windowType: getWindowType(context)); - String title = AppLocalizations.of(context)!.createTransferTooltip; - return SizedBox( - width: constraints.maxWidth - helper.getIndent() * 4, - child: FloatingActionButton( - onPressed: () => { - setState(() { - if (hasFormErrors()) { - return; - } - updateStorage(); - Navigator.popAndPushNamed(context, AppRoute.homeRoute); - }) - }, - focusNode: FocusController.getFocusNode(), - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: () => { + setState(() { + if (hasFormErrors()) { + return; + } + updateStorage(); + Navigator.popAndPushNamed(context, AppRoute.homeRoute); + }) + }, + title: AppLocalizations.of(context)!.createTransferTooltip, + icon: Icons.save, ); } diff --git a/lib/widgets/home/account_widget.dart b/lib/widgets/home/account_widget.dart index 8ba1ebd791..f4874e584a 100644 --- a/lib/widgets/home/account_widget.dart +++ b/lib/widgets/home/account_widget.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be // found in the LICENSE file. -import 'package:app_finance/_classes/app_data.dart'; import 'package:app_finance/_classes/app_route.dart'; import 'package:app_finance/_classes/currency/currency_provider.dart'; import 'package:app_finance/_classes/data/account_app_data.dart'; import 'package:app_finance/_classes/currency/exchange.dart'; +import 'package:app_finance/_classes/data/account_type.dart'; import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; import 'package:app_finance/widgets/home/base_group_widget.dart'; import 'package:app_finance/widgets/home/base_line_widget.dart'; @@ -77,7 +77,7 @@ class AccountWidget extends BaseWidget with SharedPreferencesMixin { List updateItems(context, items, summaryItem) { return items.map((o) { - o.updateContext(context); + o.setContext(context); o.progress = summaryItem.details > 0 ? exchange.reform(o.details, o.currency, exchange.getDefaultCurrency()) / summaryItem.details : o.progress; @@ -87,7 +87,7 @@ class AccountWidget extends BaseWidget with SharedPreferencesMixin { Widget buildGroupedListWidget(List items, BuildContext context, double offset) { final item = wrapBySingleEntity(items); - item.updateContext(context); + item.setContext(context); final scope = updateItems(context, items, item); return BaseGroupWidget( title: item.title, @@ -103,7 +103,7 @@ class AccountWidget extends BaseWidget with SharedPreferencesMixin { Widget buildSingleListWidget(item, BuildContext context, double offset) { item = item.first; - item.updateContext(context); + item.setContext(context); return BaseLineWidget( uuid: item.uuid, title: item.title, diff --git a/lib/widgets/home/base_group_widget.dart b/lib/widgets/home/base_group_widget.dart index 989d9995ed..20d0af42bc 100644 --- a/lib/widgets/home/base_group_widget.dart +++ b/lib/widgets/home/base_group_widget.dart @@ -36,7 +36,7 @@ class BaseGroupWidget extends StatelessWidget { Widget buildCategory(BuildContext context, int index, bool toSwap) { final item = items[index]; - item.updateContext(context); + item.setContext(context); final tooltip = StringBuffer(); tooltip.writeAll([ '${AppLocalizations.of(context)!.title}: "${item.title}"\n', diff --git a/lib/widgets/home/base_widget.dart b/lib/widgets/home/base_widget.dart index 12107d87f1..1bad15a373 100644 --- a/lib/widgets/home/base_widget.dart +++ b/lib/widgets/home/base_widget.dart @@ -38,7 +38,7 @@ class BaseWidget extends StatefulWidget { }) : super(key: key); Widget buildListWidget(item, BuildContext context, double offset) { - item.updateContext(context); + item.setContext(context); return BaseLineWidget( uuid: item.uuid ?? '', title: item.title, diff --git a/lib/widgets/home/budget_widget.dart b/lib/widgets/home/budget_widget.dart index 9f2050c708..b20a46e586 100644 --- a/lib/widgets/home/budget_widget.dart +++ b/lib/widgets/home/budget_widget.dart @@ -32,7 +32,7 @@ class BudgetWidget extends AccountWidget { @override List updateItems(context, items, summaryItem) { return items.map((o) { - o.updateContext(context); + o.setContext(context); o.progress = (summaryItem.amountLimit > 0 ? (1 - o.progress) * exchange.reform(o.amountLimit, o.currency, exchange.getDefaultCurrency()) / @@ -48,7 +48,7 @@ class BudgetWidget extends AccountWidget { @override Widget buildGroupedListWidget(List items, BuildContext context, double offset) { final item = wrapBySingleEntity(items); - item.updateContext(context); + item.setContext(context); final scope = updateItems(context, items, item); return BaseGroupWidget( title: item.title, @@ -80,7 +80,7 @@ class BudgetWidget extends AccountWidget { @override Widget buildSingleListWidget(item, BuildContext context, double offset) { item = item.first; - item.updateContext(context); + item.setContext(context); return BaseLineWidget( uuid: item.uuid, title: item.title, diff --git a/lib/widgets/home/goal_line_widget.dart b/lib/widgets/home/goal_line_widget.dart index 06ade9022b..f7740e9ce9 100644 --- a/lib/widgets/home/goal_line_widget.dart +++ b/lib/widgets/home/goal_line_widget.dart @@ -24,7 +24,7 @@ class GoalLineWidget extends StatelessWidget { final ColorScheme colorScheme = Theme.of(context).colorScheme; final TextTheme textTheme = Theme.of(context).textTheme; double screenWidth = MediaQuery.of(context).size.width - theme.getIndent() * 2; - goal.updateContext(context); + goal.setContext(context); return TapWidget( tooltip: AppLocalizations.of(context)!.goalTooltip, route: AppRoute.goalRoute, diff --git a/lib/widgets/settings/import_tab.dart b/lib/widgets/settings/import_tab.dart index eb899bf1a4..0e8c5a6021 100644 --- a/lib/widgets/settings/import_tab.dart +++ b/lib/widgets/settings/import_tab.dart @@ -7,6 +7,7 @@ import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; import 'package:app_finance/_classes/app_data.dart'; import 'package:app_finance/_classes/currency/currency_provider.dart'; import 'package:app_finance/_classes/data/account_app_data.dart'; +import 'package:app_finance/_classes/data/account_type.dart'; import 'package:app_finance/_classes/data/bill_app_data.dart'; import 'package:app_finance/_classes/data/budget_app_data.dart'; import 'package:app_finance/_classes/data/transaction_log.dart'; diff --git a/lib/widgets/start/abstract_tab.dart b/lib/widgets/start/abstract_tab.dart index 4989acdd4e..d15dd878ec 100644 --- a/lib/widgets/start/abstract_tab.dart +++ b/lib/widgets/start/abstract_tab.dart @@ -4,6 +4,7 @@ import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; import 'package:app_finance/helpers/theme_helper.dart'; +import 'package:app_finance/widgets/_forms/full_sized_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localization.dart'; @@ -23,25 +24,11 @@ abstract class AbstractTabState extends State { } Widget buildButton(BuildContext context, BoxConstraints constraints) { - final helper = ThemeHelper(windowType: getWindowType(context)); - String title = '${getButtonTitle()} (${AppLocalizations.of(context)!.goNextTooltip})'; - return SizedBox( - width: constraints.maxWidth - helper.getIndent() * 4, - child: FloatingActionButton( - onPressed: updateState, - tooltip: title, - child: Align( - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.exit_to_app_rounded), - SizedBox(height: helper.getIndent()), - Text(title, style: Theme.of(context).textTheme.headlineMedium) - ], - ), - ), - ), + return FullSizedButton( + constraints: constraints, + setState: updateState, + title: '${getButtonTitle()} (${AppLocalizations.of(context)!.goNextTooltip})', + icon: Icons.exit_to_app_rounded, ); } diff --git a/pubspec.lock b/pubspec.lock index d8d12f0a3f..f1e76012f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,7 +359,7 @@ packages: source: hosted version: "2.0.1" flutter_driver: - dependency: transitive + dependency: "direct dev" description: flutter source: sdk version: "0.0.0" @@ -503,6 +503,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 79928b6113..261c3f2e78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,10 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - + integration_test: + sdk: flutter + flutter_driver: + sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your diff --git a/test/e2e/_steps/given/clear_preferences.dart b/test/e2e/_steps/given/clear_preferences.dart new file mode 100644 index 0000000000..ca2b406102 --- /dev/null +++ b/test/e2e/_steps/given/clear_preferences.dart @@ -0,0 +1,24 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be +// found in the LICENSE file. + +import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; +import 'package:flutter_test/flutter_test.dart'; +// ignore: depend_on_referenced_packages +import 'package:gherkin/gherkin.dart'; + +import '../file_runner.dart'; +import '../../e2e_test.wrapper.dart'; + +class ClearPreferences extends Given with SharedPreferencesMixin { + @override + RegExp get pattern => RegExp(r"I clear my preferences at the start"); + + @override + Future executeStep() async { + final pref = WrapperMockSharedPreferences(); + pref.mockGetString = (value) => null; + SharedPreferencesMixin.pref = pref; + await FileRunner.tester.pumpAndSettle(); + } +} diff --git a/test/e2e/_steps/given/first_run.dart b/test/e2e/_steps/given/first_run.dart new file mode 100644 index 0000000000..87aef0d3bd --- /dev/null +++ b/test/e2e/_steps/given/first_run.dart @@ -0,0 +1,23 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; +import 'package:flutter_test/flutter_test.dart'; +// ignore: depend_on_referenced_packages +import 'package:gherkin/gherkin.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../file_runner.dart'; + +class FirstRun extends Given with SharedPreferencesMixin { + @override + RegExp get pattern => RegExp(r"I am firstly opened the app"); + + @override + Future executeStep() async { + final pref = await SharedPreferences.getInstance(); + await pref.clear(); + await FileRunner.tester.pumpAndSettle(const Duration(seconds: 2)); + await FileRunner.tester.pumpAndSettle(); + } +} diff --git a/test/e2e/_steps/given/mock_preferences.dart b/test/e2e/_steps/given/mock_preferences.dart new file mode 100644 index 0000000000..c84e255435 --- /dev/null +++ b/test/e2e/_steps/given/mock_preferences.dart @@ -0,0 +1,24 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be +// found in the LICENSE file. + +import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; +import 'package:flutter_test/flutter_test.dart'; +// ignore: depend_on_referenced_packages +import 'package:gherkin/gherkin.dart'; + +import '../file_runner.dart'; +import '../../e2e_test.wrapper.dart'; + +class MockPreferences extends Given with SharedPreferencesMixin { + @override + RegExp get pattern => RegExp(r"preferences are updated \(actually, mocked\)"); + + @override + Future executeStep() async { + final pref = WrapperMockSharedPreferences(); + pref.mockGetString = (value) => ''; + SharedPreferencesMixin.pref = pref; + await FileRunner.tester.pumpAndSettle(); + } +} diff --git a/test/e2e/_steps/when/enter_text_field.dart b/test/e2e/_steps/when/enter_text_field.dart new file mode 100644 index 0000000000..0ea539971e --- /dev/null +++ b/test/e2e/_steps/when/enter_text_field.dart @@ -0,0 +1,28 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +// ignore: depend_on_referenced_packages +import 'package:gherkin/gherkin.dart'; + +import '../file_runner.dart'; + +class EnterTextField extends When2WithWorld { + @override + RegExp get pattern => RegExp(r"I enter {string} to {string} text field"); + + @override + Future executeStep(String value, String tooltip) async { + final field = find.byWidgetPredicate((widget) { + return widget is TextField && widget.decoration?.hintText == tooltip; + }); + expect(field, findsOneWidget); + await FileRunner.tester.ensureVisible(field); + await FileRunner.tester.tap(field); + await FileRunner.tester.pump(); + await FileRunner.tester.enterText(field, value); + await FileRunner.tester.pumpAndSettle(); + } +} diff --git a/test/e2e/_steps/when/tap_defined_element.dart b/test/e2e/_steps/when/tap_defined_element.dart new file mode 100644 index 0000000000..0ba470320d --- /dev/null +++ b/test/e2e/_steps/when/tap_defined_element.dart @@ -0,0 +1,23 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +// ignore: depend_on_referenced_packages +import 'package:gherkin/gherkin.dart'; + +import '../file_runner.dart'; + +class TapDefinedElement extends When1WithWorld { + @override + RegExp get pattern => RegExp(r"I tap {string} element"); + + @override + Future executeStep(String name) async { + final el = find.text(name); + expect(el, findsOneWidget); + await FileRunner.tester.ensureVisible(el); + await FileRunner.tester.tap(el, warnIfMissed: false); + await FileRunner.tester.pumpAndSettle(); + } +} diff --git a/test/e2e/_steps/when/tap_on_num_of_defined_field.dart b/test/e2e/_steps/when/tap_on_num_of_defined_field.dart new file mode 100644 index 0000000000..f8c804f242 --- /dev/null +++ b/test/e2e/_steps/when/tap_on_num_of_defined_field.dart @@ -0,0 +1,26 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/widgets/_forms/list_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; +// ignore: depend_on_referenced_packages +import 'package:gherkin/gherkin.dart'; + +import '../file_runner.dart'; + +class TapOnNuOfDefinedField extends When2WithWorld { + @override + RegExp get pattern => RegExp(r"I tap on {int} index of {string} fields"); + + @override + Future executeStep(int order, String type) async { + Finder? list; + if (type == 'ListSelector') { + list = find.byType(ListSelector); + } + expect(list, findsWidgets); + await FileRunner.tester.ensureVisible(list!.at(order)); + await FileRunner.tester.tap(list.at(order)); + await FileRunner.tester.pumpAndSettle(); + } +} diff --git a/test/e2e/start/proceed_with_initial_configuration.feature b/test/e2e/start/proceed_with_initial_configuration.feature new file mode 100644 index 0000000000..4d9e8542b1 --- /dev/null +++ b/test/e2e/start/proceed_with_initial_configuration.feature @@ -0,0 +1,24 @@ +@start +Feature: Verify Initial Flow + + Scenario: Applying basic configuration through the start pages + Given I clear my preferences at the start + And I am on "Home" page + Then I can see "Initial Setup" component + When I tap "Save to Storage (Go Next)" button + Then I can see "Acknowledge (Go Next)" component + When I tap "Acknowledge (Go Next)" button + Given preferences are updated (actually, mocked) + Then I can see "Create new Account" component + When I tap on 0 index of "ListSelector" fields + And I tap "Bank Account" element + And I enter "Starting Page Account" to "Enter Account Identifier" text field + And I enter "1000" to "Set Balance" text field + And I tap "Create new Account" button + Then I can see "Create new Budget Category" component + When I enter "Starting Page Budget" to "Enter Budget Category Name" text field + And I enter "1000" to "Set Balance" text field + When I tap "Create new Budget Category" button + Then I can see "Accounts, total" component + And I can see "Starting Page Account" component + And I can see "Starting Page Budget" component diff --git a/test/unit/_classes/data/account_recalculation_test.dart b/test/unit/_classes/data/account_recalculation_test.dart index e9fce35fe4..08fa8ecd08 100644 --- a/test/unit/_classes/data/account_recalculation_test.dart +++ b/test/unit/_classes/data/account_recalculation_test.dart @@ -5,8 +5,8 @@ import 'package:app_finance/_classes/data/account_app_data.dart'; import 'package:app_finance/_classes/data/account_recalculation.dart'; import 'package:app_finance/_classes/currency/exchange.dart'; +import 'package:app_finance/_classes/data/account_type.dart'; import 'package:app_finance/_classes/gen/generate_with_method_setters.dart'; -import 'package:app_finance/_classes/app_data.dart'; import 'package:currency_picker/currency_picker.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; diff --git a/test/unit/_classes/data/goal_recalculation_test.dart b/test/unit/_classes/data/goal_recalculation_test.dart new file mode 100644 index 0000000000..c27be4c207 --- /dev/null +++ b/test/unit/_classes/data/goal_recalculation_test.dart @@ -0,0 +1,56 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/data/goal_app_data.dart'; +import 'package:app_finance/_classes/data/goal_recalculation.dart'; +import 'package:app_finance/_classes/gen/generate_with_method_setters.dart'; +import 'package:flutter_test/flutter_test.dart'; + +@GenerateWithMethodSetters([GoalRecalculation]) +import 'goal_recalculation_test.wrapper.dart'; + +void main() { + group('GoalRecalculation', () { + late GoalRecalculation object; + + setUp(() { + object = GoalRecalculation( + change: GoalAppData(initial: 0.0, title: ''), + initial: GoalAppData(initial: 0.0, title: ''), + ); + }); + + group('getDelta', () { + final testCases = [ + (initial: null, change: (progress: 0.5, details: 1.0, hidden: false), result: 0.0), + (initial: null, change: (progress: 0.5, details: 1.0, hidden: true), result: 0.0), + (initial: (progress: 0.5, details: 1.0), change: (progress: 0.6, details: 1.2, hidden: true), result: 0.0), + (initial: (progress: 0.1, details: 5.0), change: (progress: 0.1, details: 10.0, hidden: false), result: 1.1), + ]; + + for (var v in testCases) { + test('$v', () { + if (v.initial == null) { + object.initial = null; + } else { + object.initial!.progress = v.initial!.progress; + object.initial!.details = v.initial!.details; + } + object.change.progress = v.change.progress; + object.change.details = v.change.details; + object.change.hidden = v.change.hidden; + expect(object.getDelta(), v.result); + }); + } + }); + + test('updateGoal (change.progress: 0.0, getDelta: 0.5, result: 0.5)', () { + final obj = WrapperGoalRecalculation(change: object.change, initial: object.initial); + double result = 0.5; + obj.mockGetDelta = () => result; + expect(obj.change.progress, 0.0); + obj.updateGoal(); + expect(obj.change.progress, result); + }); + }); +} diff --git a/test/unit/_classes/data/summary_app_data_test.dart b/test/unit/_classes/data/summary_app_data_test.dart new file mode 100644 index 0000000000..5b5ae84651 --- /dev/null +++ b/test/unit/_classes/data/summary_app_data_test.dart @@ -0,0 +1,31 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/data/summary_app_data.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SummaryAppData', () { + late SummaryAppData object; + + setUp(() { + object = SummaryAppData(list: ['init'], total: 0); + }); + + group('add', () { + final current = DateTime.now(); + final testCases = [ + (item: '1', date: DateTime(current.year, current.month, current.day), listLength: 2, activeLength: 1), + (item: '1', date: DateTime(current.year, current.month - 1, current.day), listLength: 2, activeLength: 0), + ]; + + for (var v in testCases) { + test('$v', () { + object.add(v.item, updatedAt: v.date); + expect(object.list.length, v.listLength); + expect(object.listActual.length, v.activeLength); + }); + } + }); + }); +} diff --git a/test/unit/_classes/data/total_recalculation_test.dart b/test/unit/_classes/data/total_recalculation_test.dart index cc799b5e65..0a58bd7363 100644 --- a/test/unit/_classes/data/total_recalculation_test.dart +++ b/test/unit/_classes/data/total_recalculation_test.dart @@ -1,6 +1,5 @@ // Copyright 2023 The terCAD team. All rights reserved. -// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be -// found in the LICENSE file. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. import 'dart:collection'; diff --git a/test/unit/_classes/focus_controller_test.dart b/test/unit/_classes/focus_controller_test.dart new file mode 100644 index 0000000000..7d0f0631fb --- /dev/null +++ b/test/unit/_classes/focus_controller_test.dart @@ -0,0 +1,63 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/focus_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FocusController', () { + setUp(() { + FocusController.dispose(); + FocusController.getController(FocusController); + }); + + group('onEditingComplete', () { + final testCases = [ + ( + fnCalls: 1, + resultCursor: 0, + editComplete: 0, + focus: -1, + checkFocus: 0, + value: null, + result: true, + isLast: true, + ), + ( + fnCalls: 10, + resultCursor: 9, + editComplete: 5, + focus: 6, + checkFocus: 6, + value: null, + result: true, + isLast: false, + ), + ( + fnCalls: 10, + resultCursor: 9, + editComplete: 5, + focus: 6, + checkFocus: 6, + value: 'test', + result: false, + isLast: false, + ), + ]; + + for (var v in testCases) { + test('$v', () { + expect(FocusController.current, FocusController.DEFAULT); + for (int i = 0; i < v.fnCalls; i++) { + FocusController.getFocusNode(); + } + expect(FocusController.current, v.resultCursor); + FocusController.onEditingComplete(v.editComplete); + expect(FocusController.focus, v.focus); + expect(FocusController.isFocused(v.checkFocus, v.value), v.result); + expect(FocusController.isLast(), v.isLast); + }); + } + }); + }); +} diff --git a/test/unit/_mixins/formatter_mixin_test.dart b/test/unit/_mixins/formatter_mixin_test.dart new file mode 100644 index 0000000000..8016f5731b --- /dev/null +++ b/test/unit/_mixins/formatter_mixin_test.dart @@ -0,0 +1,40 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/gen/generate_with_method_setters.dart'; +import 'package:app_finance/_mixins/formatter_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +import 'formatter_mixin_test.mocks.dart'; + +@GenerateWithMethodSetters([FormatterMixin]) +import 'formatter_mixin_test.wrapper.dart'; + +void main() { + group('FormatterMixin', () { + final mock = MockBuildContext(); + final object = WrapperFormatterMixin(); + + setUp(() { + object.setContext(mock); + }); + + group('getDateFormatted', () { + final testCases = [ + (date: [2023, 8, 13], locale: null, result: 'Sun, 8/13/2023'), + //(date: [2023, 8, 13], locale: 'de', result: 'Sonne, 13/8/2023'), // Err: Locale data has not been initialized + ]; + + for (var v in testCases) { + test('$v', () { + object.mockGetLocale = () => v.locale; + final date = DateTime(v.date[0], v.date[1], v.date[2]); + expect(object.getDateFormatted(date), v.result); + }); + } + }); + }); +} diff --git a/test_driver/perf_driver.dart b/test_driver/perf_driver.dart new file mode 100644 index 0000000000..904051ad6f --- /dev/null +++ b/test_driver/perf_driver.dart @@ -0,0 +1,21 @@ +// Taken from: https://docs.flutter.dev/cookbook/testing/integration/profiling + +import 'package:flutter_driver/flutter_driver.dart' as driver; +import 'package:integration_test/integration_test_driver.dart'; + +Future main() { + return integrationDriver( + responseDataCallback: (data) async { + if (data != null) { + final timeline = driver.Timeline.fromJson(data['timeline']); + final summary = driver.TimelineSummary.summarize(timeline); + await summary.writeTimelineToFile( + 'timeline', + pretty: true, + includeSummary: true, + destinationDirectory: './coverage/', + ); + } + }, + ); +} diff --git a/test_driver/warm_up.dart b/test_driver/warm_up.dart new file mode 100644 index 0000000000..225f025486 --- /dev/null +++ b/test_driver/warm_up.dart @@ -0,0 +1,7 @@ +// Taken from: https://docs.flutter.dev/cookbook/testing/integration/profiling + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() { + return integrationDriver(); +} diff --git a/test_driver/warm_up_test.dart b/test_driver/warm_up_test.dart new file mode 100644 index 0000000000..3d5c59cc9b --- /dev/null +++ b/test_driver/warm_up_test.dart @@ -0,0 +1,32 @@ +// Copyright 2023 The terCAD team. All rights reserved. +// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. + +import 'package:app_finance/_classes/app_data.dart'; +import 'package:app_finance/_classes/app_theme.dart'; +import 'package:app_finance/_mixins/shared_preferences_mixin.dart'; +import 'package:app_finance/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferencesMixin.pref = await SharedPreferences.getInstance(); + + testWidgets('Warm-up', (WidgetTester tester) async { + await tester.pumpWidget(MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => AppData(), + ), + ChangeNotifierProvider( + create: (_) => AppTheme(ThemeMode.system), + ), + ], + child: const MyApp(), + )); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); +}