diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 54d835a..5e310c8 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -26,6 +26,10 @@ jobs: - uses: subosito/flutter-action@v2 with: flutter-version: "3.35.2" + - name: Install Linux dependencies + run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev + - name: Install Xvfb + run: sudo apt-get install -y xvfb - run: flutter pub get - run: | if [ -d "test" ]; then @@ -33,6 +37,12 @@ jobs: else echo "No tests found, skipping..." fi + - run: | + if [ -d "integration_test" ]; then + xvfb-run -s '-screen 0 1024x768x24' flutter test integration_test -d linux + else + echo "No integration tests found, skipping..." + fi build: name: Build diff --git a/integration_test/app_flow_test.dart b/integration_test/app_flow_test.dart new file mode 100644 index 0000000..b796350 --- /dev/null +++ b/integration_test/app_flow_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutterproject/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('login then create task', (WidgetTester tester) async { + app.main(); + + // wait for splash screen to navigate to login + await tester.pumpAndSettle(const Duration(seconds: 4)); + + // fill login form + await tester.enterText( + find.byType(TextFormField).at(0), 'admin@todolist.com'); + await tester.enterText( + find.byType(TextFormField).at(1), '123456'); + await tester.tap(find.text('Se connecter')); + await tester.pumpAndSettle(); + + // wait for tasks to load + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + // open form + await tester.tap(find.text('Nouvelle tâche')); + await tester.pumpAndSettle(); + + // create task + await tester.enterText( + find.byType(TextFormField).first, 'Tâche intégration'); + await tester.tap(find.text('Créer')); + await tester.pumpAndSettle(); + + expect(find.text('Tâche intégration'), findsOneWidget); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 89a2309..58d2b10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + 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 diff --git a/test/auth_service_test.dart b/test/auth_service_test.dart new file mode 100644 index 0000000..7007834 --- /dev/null +++ b/test/auth_service_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutterproject/features/auth/data/auth_service.dart'; + +void main() { + late AuthService service; + + setUp(() { + service = AuthService(); + }); + + test('login succeeds with valid credentials', () async { + final result = await service.login('admin@todolist.com', '123456'); + expect(result.success, isTrue); + expect(service.isLoggedIn, isTrue); + expect(service.currentUserEmail, 'admin@todolist.com'); + }); + + test('login fails with invalid credentials', () async { + final result = await service.login('wrong@example.com', 'bad'); + expect(result.success, isFalse); + expect(service.isLoggedIn, isFalse); + }); + + test('logout resets state', () async { + await service.login('admin@todolist.com', '123456'); + await service.logout(); + expect(service.isLoggedIn, isFalse); + expect(service.currentUserEmail, isNull); + }); +} diff --git a/test/task_list_widget_test.dart b/test/task_list_widget_test.dart new file mode 100644 index 0000000..bc87c7c --- /dev/null +++ b/test/task_list_widget_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:flutterproject/features/tasks/domain/models/task.dart'; +import 'package:flutterproject/features/tasks/presentation/providers/task_provider.dart'; + +class _TaskList extends StatelessWidget { + const _TaskList(); + + @override + Widget build(BuildContext context) { + final tasks = context.watch().allTasks; + return ListView( + children: tasks.map((t) => Text(t.title)).toList(), + ); + } +} + +void main() { + testWidgets('displays tasks from provider', (WidgetTester tester) async { + final provider = TaskProvider(); + provider.addTask(Task(id: '1', title: 'Test 1', createdAt: DateTime.now())); + provider.addTask(Task(id: '2', title: 'Test 2', createdAt: DateTime.now())); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: provider, + child: const MaterialApp(home: Scaffold(body: _TaskList())), + ), + ); + + expect(find.text('Test 1'), findsOneWidget); + expect(find.text('Test 2'), findsOneWidget); + }); +} diff --git a/test/task_modal_test.dart b/test/task_modal_test.dart new file mode 100644 index 0000000..4fa74f4 --- /dev/null +++ b/test/task_modal_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:flutterproject/features/tasks/presentation/providers/task_provider.dart'; +import 'package:flutterproject/features/tasks/presentation/widgets/task_modal.dart'; + +void main() { + testWidgets('TaskModal validates empty title', (WidgetTester tester) async { + tester.binding.window.physicalSizeTestValue = const Size(800, 1200); + tester.binding.window.devicePixelRatioTestValue = 1.0; + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + addTearDown(tester.binding.window.clearDevicePixelRatioTestValue); + + await tester.pumpWidget( + ChangeNotifierProvider( + create: (_) => TaskProvider(), + child: const MaterialApp(home: Scaffold(body: TaskModal())), + ), + ); + + await tester.ensureVisible(find.text('Créer')); + await tester.tap(find.text('Créer')); + await tester.pump(); + + expect(find.text('Le titre est obligatoire'), findsOneWidget); + }); +} diff --git a/test/task_model_test.dart b/test/task_model_test.dart new file mode 100644 index 0000000..13c3d54 --- /dev/null +++ b/test/task_model_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutterproject/features/tasks/domain/models/task.dart'; +import 'package:flutterproject/features/tasks/presentation/providers/task_provider.dart'; + +void main() { + test('toggleCompleted switches the completion state', () { + final task = Task( + id: '1', + title: 'Demo', + createdAt: DateTime(2024, 1, 1), + isCompleted: false, + ); + final toggled = task.toggleCompleted(); + expect(toggled.isCompleted, isTrue); + expect(task.isCompleted, isFalse); + }); + + test('TaskStats calculates completion rate', () { + const stats = TaskStats(total: 4, completed: 1, pending: 3, highPriority: 0); + expect(stats.completionRate, closeTo(0.25, 0.001)); + }); +}