From 8b0b52c4a3af65b49071ec150e4f50a872c233c2 Mon Sep 17 00:00:00 2001 From: Jaime Date: Tue, 6 Aug 2024 13:11:53 +0200 Subject: [PATCH 1/3] feat: generate random data --- lib/demo/widgets/transactions_table.dart | 2 +- .../bloc/financial_data_bloc.dart | 44 ++++++++++++----- .../bloc/financial_data_state.dart | 49 +++++++++++++++++-- lib/l10n/arb/app_en.arb | 36 ++++++++++++++ test/demo/demo_page_test.dart | 6 +-- .../bloc/financial_data_bloc_test.dart | 28 +++++++---- .../bloc/financial_data_state_test.dart | 11 +++-- .../widgets/goal_progress_indicator_test.dart | 6 +-- 8 files changed, 143 insertions(+), 39 deletions(-) diff --git a/lib/demo/widgets/transactions_table.dart b/lib/demo/widgets/transactions_table.dart index 7ce5b13..06173e9 100644 --- a/lib/demo/widgets/transactions_table.dart +++ b/lib/demo/widgets/transactions_table.dart @@ -37,7 +37,7 @@ class TransactionsTable extends StatelessWidget { for (final transaction in transactions) ListTile( contentPadding: EdgeInsets.zero, - title: Text(transaction.title), + title: Text(transaction.type.title(context)), subtitle: DefaultTextStyle( style: textTheme.bodyMedium!.copyWith( color: colorScheme.onSurfaceVariant.withOpacity(0.8), diff --git a/lib/financial_data/bloc/financial_data_bloc.dart b/lib/financial_data/bloc/financial_data_bloc.dart index ea9467f..fcfe1b7 100644 --- a/lib/financial_data/bloc/financial_data_bloc.dart +++ b/lib/financial_data/bloc/financial_data_bloc.dart @@ -1,40 +1,51 @@ +import 'dart:math' show Random; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; +import 'package:financial_dashboard/l10n/l10n.dart'; +import 'package:flutter/widgets.dart'; part 'financial_data_event.dart'; part 'financial_data_state.dart'; class FinancialDataBloc extends Bloc { - FinancialDataBloc() : super(const FinancialDataState()) { + FinancialDataBloc({ + Random? random, + }) : _random = random ?? Random(), + super(const FinancialDataState()) { on(_onLoadFinancialData); } + final Random _random; + Future _onLoadFinancialData( FinancialDataRequested event, Emitter emit, ) async { + final savingsDataPoints = createSampleData(random: _random); + final index = _random.nextInt(savingsDataPoints.length); + final currentSavings = savingsDataPoints[index].value; + final monthlySpendingLimitGoal = (_random.nextDouble() * 1000) + 3000; + emit( state.copyWith( - currentSavings: 234567.91, - savingsDataPoints: createSampleData(), - monthlySpendingLimitGoal: 3210.55, - transactions: [ - const Transaction(title: 'Paycheck', amount: 3000), - const Transaction(title: 'Rent', amount: -1050.20), - const Transaction(title: 'Food', amount: -670.50), - ], + currentSavings: currentSavings, + savingsDataPoints: savingsDataPoints, + monthlySpendingLimitGoal: monthlySpendingLimitGoal, + transactions: createSampleTransactions(random: _random), ), ); } } @visibleForTesting -List createSampleData() { +List createSampleData({Random? random}) { + random ??= Random(); + final data = []; var value = 100000.0; for (var age = 25; age <= 90; age++) { - const toAdd = 10000; + final toAdd = random.nextInt(12000); value += toAdd; data.add( SavingsDataPoint(age: age, value: value), @@ -42,3 +53,12 @@ List createSampleData() { } return data; } + +@visibleForTesting +List createSampleTransactions({Random? random}) { + random ??= Random(); + + return TransactionType.values + .map((type) => Transaction(type: type, amount: type.amount(random!))) + .toList(); +} diff --git a/lib/financial_data/bloc/financial_data_state.dart b/lib/financial_data/bloc/financial_data_state.dart index cca431c..5d4769c 100644 --- a/lib/financial_data/bloc/financial_data_state.dart +++ b/lib/financial_data/bloc/financial_data_state.dart @@ -50,17 +50,60 @@ class SavingsDataPoint extends Equatable { List get props => [age, value]; } +enum TransactionType { + paycheck(4000, 5500), + rent(1000, 2000), + groceries(320, 500), + car(350, 600), + gym(40, 90), + other(200, 350); + + const TransactionType(this.min, this.max); + + final int min; + final int max; +} + +extension TransactionTypeX on TransactionType { + List get incomeTypes => [TransactionType.paycheck]; + + double amount(Random random) { + final value = random.nextDouble() * (max - min) + min; + + if (incomeTypes.contains(this)) return value; + return -value; + } + + String title(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case TransactionType.paycheck: + return l10n.paycheck; + case TransactionType.groceries: + return l10n.groceries; + case TransactionType.rent: + return l10n.rent; + case TransactionType.car: + return l10n.car; + case TransactionType.gym: + return l10n.gym; + case TransactionType.other: + return l10n.other; + } + } +} + class Transaction extends Equatable { const Transaction({ - required this.title, + required this.type, required this.amount, }); - final String title; + final TransactionType type; final double amount; @override - List get props => [title, amount]; + List get props => [type, amount]; } extension TransactionListX on List { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 73ca6f3..e9e5a95 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -47,5 +47,41 @@ "description": "Text shown when there is no data available", "type": "text", "placeholders": {} + }, + "paycheck": "Paycheck", + "@paycheck":{ + "description": "Label for paycheck", + "type": "text", + "placeholders": {} + }, + "groceries": "Groceries", + "@groceries":{ + "description": "Label for groceries", + "type": "text", + "placeholders": {} + }, + "rent": "Rent", + "@rent":{ + "description": "Label for rent", + "type": "text", + "placeholders": {} + }, + "car": "Car loan", + "@car":{ + "description": "Label for car", + "type": "text", + "placeholders": {} + }, + "gym": "Gym membership", + "@gym":{ + "description": "Label for gym", + "type": "text", + "placeholders": {} + }, + "other": "Other", + "@other":{ + "description": "Label for other", + "type": "text", + "placeholders": {} } } diff --git a/test/demo/demo_page_test.dart b/test/demo/demo_page_test.dart index 892566e..03bf439 100644 --- a/test/demo/demo_page_test.dart +++ b/test/demo/demo_page_test.dart @@ -37,11 +37,7 @@ void main() { currentSavings: 123456, savingsDataPoints: createSampleData(), monthlySpendingLimitGoal: 1000, - transactions: [ - const Transaction(title: 'Paycheck', amount: 3000), - const Transaction(title: 'Rent', amount: -1050.20), - const Transaction(title: 'Food', amount: -670.50), - ], + transactions: createSampleTransactions(), ), ); }); diff --git a/test/financial_data/bloc/financial_data_bloc_test.dart b/test/financial_data/bloc/financial_data_bloc_test.dart index 372e4cf..b919615 100644 --- a/test/financial_data/bloc/financial_data_bloc_test.dart +++ b/test/financial_data/bloc/financial_data_bloc_test.dart @@ -1,23 +1,33 @@ +import 'dart:math' show Random; + import 'package:bloc_test/bloc_test.dart'; import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockRandom extends Mock implements Random {} void main() { group('FinancialDataBloc', () { + late Random random; + + setUp(() { + random = _MockRandom(); + + when(() => random.nextInt(any())).thenReturn(0); + when(() => random.nextDouble()).thenReturn(0); + }); + blocTest( 'loads financial data when $FinancialDataRequested is added', - build: FinancialDataBloc.new, + build: () => FinancialDataBloc(random: random), act: (bloc) => bloc.add(FinancialDataRequested()), expect: () => [ FinancialDataState( - currentSavings: 234567.91, - savingsDataPoints: createSampleData(), - monthlySpendingLimitGoal: 3210.55, - transactions: [ - const Transaction(title: 'Paycheck', amount: 3000), - const Transaction(title: 'Rent', amount: -1050.20), - const Transaction(title: 'Food', amount: -670.50), - ], + currentSavings: 100000, + savingsDataPoints: createSampleData(random: random), + monthlySpendingLimitGoal: 3000, + transactions: createSampleTransactions(random: random), ), ], ); diff --git a/test/financial_data/bloc/financial_data_state_test.dart b/test/financial_data/bloc/financial_data_state_test.dart index a76680a..562235f 100644 --- a/test/financial_data/bloc/financial_data_state_test.dart +++ b/test/financial_data/bloc/financial_data_state_test.dart @@ -32,7 +32,10 @@ void main() { test('copies transactions', () { final state = FinancialDataState(); - final transaction = Transaction(title: 'test', amount: 123.45); + final transaction = Transaction( + type: TransactionType.other, + amount: 123.45, + ); final newState = state.copyWith(transactions: [transaction]); expect(newState.transactions, equals([transaction])); @@ -53,9 +56,9 @@ void main() { group('Transaction', () { test('supports value equality', () { - const pointA = Transaction(title: 'test', amount: 100); - const secondPointA = Transaction(title: 'test', amount: 100); - const pointB = Transaction(title: 'test-two', amount: 200); + const pointA = Transaction(type: TransactionType.gym, amount: 100); + const secondPointA = Transaction(type: TransactionType.gym, amount: 100); + const pointB = Transaction(type: TransactionType.other, amount: 200); expect(pointA, equals(secondPointA)); expect(pointA, isNot(equals(pointB))); diff --git a/test/src/ui/widgets/goal_progress_indicator_test.dart b/test/src/ui/widgets/goal_progress_indicator_test.dart index a1608ae..798167d 100644 --- a/test/src/ui/widgets/goal_progress_indicator_test.dart +++ b/test/src/ui/widgets/goal_progress_indicator_test.dart @@ -28,11 +28,7 @@ void main() { when(() => financialDataBloc.state).thenReturn( FinancialDataState( monthlySpendingLimitGoal: 1000, - transactions: [ - const Transaction(title: 'Paycheck', amount: 3000), - const Transaction(title: 'Rent', amount: -1050.20), - const Transaction(title: 'Food', amount: -670.50), - ], + transactions: createSampleTransactions(), ), ); }); From 98beed3c877d1064a908d0071cc92ba0db5dc3ac Mon Sep 17 00:00:00 2001 From: Jaime Date: Tue, 6 Aug 2024 13:26:25 +0200 Subject: [PATCH 2/3] feat: generate random data on pull to refresh --- lib/demo/widgets/app_one.dart | 5 +++++ lib/demo/widgets/app_three.dart | 5 +++++ lib/demo/widgets/app_two.dart | 5 +++++ test/demo/demo_page_test.dart | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/lib/demo/widgets/app_one.dart b/lib/demo/widgets/app_one.dart index 13dd122..349f536 100644 --- a/lib/demo/widgets/app_one.dart +++ b/lib/demo/widgets/app_one.dart @@ -1,6 +1,8 @@ import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class AppOne extends StatelessWidget { const AppOne({super.key}); @@ -12,6 +14,9 @@ class AppOne extends StatelessWidget { final colorScheme = theme.colorScheme; return AppScaffold( + onRefresh: () async { + context.read().add(const FinancialDataRequested()); + }, predictionChart: Card( child: Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/demo/widgets/app_three.dart b/lib/demo/widgets/app_three.dart index 72a171a..14833c7 100644 --- a/lib/demo/widgets/app_three.dart +++ b/lib/demo/widgets/app_three.dart @@ -1,6 +1,8 @@ import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class AppThree extends StatelessWidget { const AppThree({super.key}); @@ -12,6 +14,9 @@ class AppThree extends StatelessWidget { final colorScheme = theme.colorScheme; return AppScaffold( + onRefresh: () async { + context.read().add(const FinancialDataRequested()); + }, showTitleInAppBar: true, predictionChart: Card( child: Padding( diff --git a/lib/demo/widgets/app_two.dart b/lib/demo/widgets/app_two.dart index 1c00cb2..1fa9582 100644 --- a/lib/demo/widgets/app_two.dart +++ b/lib/demo/widgets/app_two.dart @@ -1,6 +1,8 @@ import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class AppTwo extends StatelessWidget { const AppTwo({super.key}); @@ -13,6 +15,9 @@ class AppTwo extends StatelessWidget { const spacing = AppSpacing.xxlg * 2; return AppScaffold( + onRefresh: () async { + context.read().add(const FinancialDataRequested()); + }, extendBodyBehindAppBar: true, titlePadding: const EdgeInsets.symmetric( horizontal: AppSpacing.xlg, diff --git a/test/demo/demo_page_test.dart b/test/demo/demo_page_test.dart index 03bf439..6e2ad6b 100644 --- a/test/demo/demo_page_test.dart +++ b/test/demo/demo_page_test.dart @@ -74,5 +74,40 @@ void main() { ); expect(find.byType(AppThree), findsOneWidget); }); + + group('adds FinancialDataRequested event when pulling to refresh', () { + for (final flavor in AppFlavor.values) { + testWidgets( + 'in $flavor', + (tester) async { + when(() => flavorCubit.state).thenReturn(flavor); + + await tester.pumpExperience( + const DemoView(), + flavorCubit: flavorCubit, + themeModeCubit: themeModeCubit, + financialDataBloc: financialDataBloc, + ); + await tester.pumpAndSettle(); + + final widgetToFling = find.byType(AppScaffold); + expect(widgetToFling, findsOneWidget); + + await tester.fling(widgetToFling, const Offset(0, 500), 1000); + await tester.pump(); + + // Finish the scroll animation + await tester.pump(const Duration(seconds: 1)); + // Finish the indicator settle animation + await tester.pump(const Duration(seconds: 1)); + // Finish the indicator hide animation + await tester.pump(const Duration(seconds: 1)); + + verify(() => financialDataBloc.add(const FinancialDataRequested())) + .called(1); + }, + ); + } + }); }); } From 1f0ca69d7cb5a0c7b49ed838d1679718687efb4e Mon Sep 17 00:00:00 2001 From: Jaime Date: Tue, 6 Aug 2024 16:43:59 +0200 Subject: [PATCH 3/3] docs: follow conventions --- test/demo/demo_page_test.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/demo/demo_page_test.dart b/test/demo/demo_page_test.dart index 6e2ad6b..0df751c 100644 --- a/test/demo/demo_page_test.dart +++ b/test/demo/demo_page_test.dart @@ -75,7 +75,7 @@ void main() { expect(find.byType(AppThree), findsOneWidget); }); - group('adds FinancialDataRequested event when pulling to refresh', () { + group('adds $FinancialDataRequested event when pulling to refresh', () { for (final flavor in AppFlavor.values) { testWidgets( 'in $flavor', @@ -94,13 +94,12 @@ void main() { expect(widgetToFling, findsOneWidget); await tester.fling(widgetToFling, const Offset(0, 500), 1000); - await tester.pump(); - // Finish the scroll animation + // Finish the scroll animation. await tester.pump(const Duration(seconds: 1)); - // Finish the indicator settle animation + // Finish the indicator settle animation. await tester.pump(const Duration(seconds: 1)); - // Finish the indicator hide animation + // Finish the indicator hide animation. await tester.pump(const Duration(seconds: 1)); verify(() => financialDataBloc.add(const FinancialDataRequested()))