Skip to content

Commit

Permalink
fix: requested changes
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolantean committed Apr 23, 2024
1 parent 8cbc4dc commit deb4d79
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 71 deletions.
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Xmartlabs' Flutter template

## Arch Overview
## Arch Overview

The project is divided into two main folders:

- The UI contains all app screens.
- The Core contains the models and the data layer.

Expand All @@ -27,34 +28,47 @@ These components are injected in the Cubits using [get_it][get_it].
## Project Overview

### Assets

The [`/assets/`](./assets) folder contains the assets used by the application, such as images, fonts, and other files.

### Environments

The environment variables are defined in the `default.env` file located in [`/environments/`](./environments) folder.
You can read more information about the environment variables in the [README.md](./environments/README.md) file.

### Testing

#### Mocks

For moking the projects uses [mocktail][mocktail], a library inspired on [mockito][mockito] which deletes the code generation part.

#### Integration test

The integration tests for the project are defined in [integration_test][integration_test]. Dart package integration_test is used for the implementation. Are included two tests for the Sign In flow, covering both successful and failed sign-in attempts.

## Project Setup

The project setup is based on some plugins which generate the required native code.

You can use [project_setup.sh](scripts/project_setup.sh) to reload all project setups.

### Flavor setup: Project name, properties BundleId & Application id
This information is set using [flavorizr], a flutter utility to easily create flavors in your flutter application.

This information is set using [flavorizr], a flutter utility to easily create flavors in your flutter application.
To change it go to `flavorizr` section in the [pubspec] file.

For example, to add a new flavour, you can do something like:

```yaml
flavorizr:
flavors:
qa:
app:
name: 'My Project - QA'
name: "My Project - QA"
android:
applicationId: 'com.xmartlabs.myproject.qa'
applicationId: "com.xmartlabs.myproject.qa"
ios:
bundleId: 'com.xmartlabs.myproject.qa'
bundleId: "com.xmartlabs.myproject.qa"
```
After a change is made, you need to regenerate your native files.
Expand All @@ -70,7 +84,6 @@ To change it go to `flutter_icons` section in the [pubspec] file.
After a change is made, you need to regenerate your native files.
You can do that by executing `flutter pub run flutter_launcher_icons:main`.


### Splash screen

Splash screen is generated using [flutter_native_splash].
Expand All @@ -80,13 +93,14 @@ After a change is made, you need to regenerate your native files.
You can do that by executing `flutter pub run flutter_native_splash:create`.

Although you can setup a bunch of features in this library, it doesn't provide a way to display animations.
If you need a more personalized splash screen, you can edit the native code or just remove this library.
If you need a more personalized splash screen, you can edit the native code or just remove this library.

### Code generation

Code generation is created using `build_runner` package.\
To configure this package edit the `build.yaml`\
To add new files to watch for code generation add the following lines:

```
targets:
$default:
Expand All @@ -98,6 +112,7 @@ targets:
# Example glob for only the Dart files under `lib/models`
- lib/models/*.dart
```
To create generated code run `clean_up.sh` under [scripts] folder or the following command: `flutter pub run build_runner build --delete-conflicting-outputs`

### Pre Push config
Expand All @@ -117,3 +132,6 @@ In order to setup pre-push hook you need to go to the root of the project and ru
[data_source_folder]: https://github.com/xmartlabs/flutter-template/tree/main/lib/core/source
[get_it]: https://pub.dev/packages/get_it
[scripts]: https://github.com/xmartlabs/flutter-template/tree/main/scripts
[integration_test]: https://github.com/xmartlabs/flutter-template/tree/main/intgration_test
[mocktail]: https://pub.dev/packages/mocktail
[mockito]: https://pub.dev/packages/mockito
30 changes: 20 additions & 10 deletions integration_test/common/general_helpers.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_template/core/di/di_provider.dart';
import 'package:flutter_template/core/repository/session_repository.dart';
import 'package:flutter_template/core/source/auth_remote_source.dart';
import 'package:flutter_template/core/source/project_remote_source.dart';
import 'package:flutter_template/main.dart' as app;
import 'package:flutter_template/ui/router/app_router.dart';
import 'package:flutter_test/flutter_test.dart';

import 'repository_mocks.dart';

Future<void> commonSetup(
MockSessionRepository mockSessionRepository,
AppRouter appRouter,
) async {
Future<void> commonSetup({
required MockAuthRemoteSource mockAuthRemoteSource,
required MockProjectRemoteSource mockProjectRemoteSource,
}) async {
await app.initSdks();

DiProvider.instance.unregister<SessionRepository>();
DiProvider.instance.unregister<AppRouter>();
DiProvider.instance.unregister<AuthRemoteSource>();
DiProvider.instance.unregister<ProjectRemoteSource>();

DiProvider.instance.registerSingleton<AuthRemoteSource>(mockAuthRemoteSource);
DiProvider.instance
.registerSingleton<SessionRepository>(mockSessionRepository);
DiProvider.instance.registerSingleton<AppRouter>(appRouter);
.registerSingleton<ProjectRemoteSource>(mockProjectRemoteSource);
}

extension WidgetTesterExtension on WidgetTester {
BuildContext contextOfType<T extends Widget>() {
final finder = find.byType(T);
expect(finder, findsOneWidget);
return element(finder);
}
}
7 changes: 5 additions & 2 deletions integration_test/common/repository_mocks.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'package:flutter_template/core/repository/session_repository.dart';
import 'package:flutter_template/core/source/auth_remote_source.dart';
import 'package:flutter_template/core/source/project_remote_source.dart';
import 'package:mocktail/mocktail.dart';

class MockSessionRepository extends Mock implements SessionRepository {}
class MockProjectRemoteSource extends Mock implements ProjectRemoteSource {}

class MockAuthRemoteSource extends Mock implements AuthRemoteSource {}
95 changes: 43 additions & 52 deletions integration_test/test/signin_test.dart
Original file line number Diff line number Diff line change
@@ -1,102 +1,93 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_template/core/common/network_exceptions.dart';
import 'package:flutter_template/core/di/di_provider.dart';
import 'package:flutter_template/core/model/authentication_status.dart';
import 'package:flutter_template/core/model/service/auth_models.dart';
import 'package:flutter_template/core/model/user.dart';
import 'package:flutter_template/main.dart' as app;
import 'package:flutter_template/ui/extensions/context_extensions.dart';
import 'package:flutter_template/ui/router/app_router.dart';
import 'package:flutter_template/ui/section/error_handler/global_event_handler_cubit.dart';
import 'package:flutter_template/ui/signin/signin_screen.dart';
import 'package:flutter_template/ui/welcome/welcome_screen.dart';
import 'package:mocktail/mocktail.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rxdart/rxdart.dart';
import 'package:mocktail/mocktail.dart';

import '../common/general_helpers.dart';
import '../common/repository_mocks.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

late MockSessionRepository mockSessionRepository;
late AppRouter appRouter;
late BuildContext context;
late StreamController<AuthenticationStatus> statusController;
late MockAuthRemoteSource mockAuthRemoteSource;
late MockProjectRemoteSource mockProjectRemoteSource;

setUp(() async {
mockSessionRepository = MockSessionRepository();
statusController = BehaviorSubject()
..add(AuthenticationStatus.unauthenticated);
when(() => mockSessionRepository.status)
.thenAnswer((_) => statusController.stream);
appRouter = AppRouter(mockSessionRepository);
await commonSetup(mockSessionRepository, appRouter);
mockAuthRemoteSource = MockAuthRemoteSource();
mockProjectRemoteSource = MockProjectRemoteSource();
await commonSetup(
mockAuthRemoteSource: mockAuthRemoteSource,
mockProjectRemoteSource: mockProjectRemoteSource,
);
});

tearDown(() {
DiProvider.instance.reset();
statusController.close();
});
group('SignIn screen tests', () {
const email = "[email protected]";
const password = "test";

testWidgets('SignIn with wrong credentials', (WidgetTester tester) async {
mockSignInUser(false, mockSessionRepository, statusController);
await Future.delayed(const Duration(seconds: 2));
when(() => mockAuthRemoteSource.signIn(email, password)).thenAnswer(
(_) => Future.error(
const NetworkException.unauthorizedRequest("Unauthorized"),
),
);

await tester.pumpWidget(const app.MyApp());
await tester.pumpAndSettle();
context = tester.element(find.byType(SignInScreen));

final context = tester.contextOfType<SignInScreen>();
await tester.enterText(
find.widgetWithText(TextField, context.localizations.mail),
'[email protected]',
email,
);
await tester.enterText(
find.widgetWithText(TextField, context.localizations.password),
'1234',
password,
);
await tester.tap(
find.widgetWithText(TextButton, context.localizations.sign_in),
);
await tester
.tap(find.widgetWithText(TextButton, context.localizations.sign_in));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
});
testWidgets('SignIn with correct credentials', (WidgetTester tester) async {
mockSignInUser(true, mockSessionRepository, statusController);
await Future.delayed(const Duration(seconds: 2));
when(() => mockAuthRemoteSource.signIn(email, password)).thenAnswer(
(_) => Future.value(
SignInResponse(
accessToken: "testing-token",
user: User(email: "[email protected]"),
),
),
);
when(() => mockProjectRemoteSource.getProjects())
.thenAnswer((_) => Future.value([]));

await tester.pumpWidget(const app.MyApp());
await tester.pumpAndSettle();
context = tester.element(find.byType(SignInScreen));

final context = tester.contextOfType<SignInScreen>();
await tester.enterText(
find.widgetWithText(TextField, context.localizations.mail),
'[email protected]',
email,
);
await tester.enterText(
find.widgetWithText(TextField, context.localizations.password),
'xmartlabs',
password,
);
await tester
.tap(find.widgetWithText(TextButton, context.localizations.sign_in));
await tester.pumpAndSettle();
await Future.delayed(const Duration(seconds: 2));
expect(find.byType(WelcomeScreen), findsOneWidget);
});
});
}

void mockSignInUser(
bool correctCredentials,
MockSessionRepository mockSessionRepository,
StreamController<AuthenticationStatus> statusController,
) =>
when(
() => mockSessionRepository.signInUser(
email: '[email protected]',
password: correctCredentials ? 'xmartlabs' : '1234',
),
).thenAnswer(
correctCredentials
? (_) async {
statusController.add(AuthenticationStatus.authenticated);
}
: (_) => Future.error(
const UnknownError('Ops! Something went wrong'),
),
);
3 changes: 3 additions & 0 deletions scripts/integration_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test/signin_test.dart --flavor dev
3 changes: 3 additions & 0 deletions test_driver/integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

0 comments on commit deb4d79

Please sign in to comment.