diff --git a/.github/workflows/_build-android-app.yml b/.github/workflows/_build-android-app.yml index f0b181e3..4af81f3d 100644 --- a/.github/workflows/_build-android-app.yml +++ b/.github/workflows/_build-android-app.yml @@ -113,5 +113,5 @@ jobs: serviceCredentialsFileContent: ${{secrets.CREDENTIAL_FILE_CONTENT}} groups: testers file: ./apps/multichoice/build/app/outputs/flutter-apk/app-${{ inputs.environment_flag }}.apk - releaseNotesFile: ./apps/multichoice/CHANGELOG.txt + releaseNotesFile: ./CHANGELOG.md debug: true diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 6f0ad6f1..89a1987c 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -57,22 +57,18 @@ jobs: uses: ./.github/actions/setup-java-flutter - name: Run tests and generate coverage - run: melos coverage:core - - - name: Aggregate coverage report + run: melos coverage:all + + - name: Debug coverage files run: | - flutter pub global activate coverage - flutter pub global run coverage:format_coverage \ - --lcov \ - --in=packages/core/coverage \ - --out=coverage/lcov.info \ - --report-on=apps/multichoice/lib,packages/core/lib + ls -la apps/multichoice/coverage/ + ls -la packages/core/coverage/ - # if requied, change 'Aggregate coverage report' with 'Move coverage report' - # - name: Move coverage report - # run: | - # mkdir -p coverage - # cp packages/core/coverage/lcov.info coverage/lcov.info + - name: Merge coverage reports + run: | + mkdir -p coverage + cat apps/multichoice/coverage/lcov.info packages/core/coverage/lcov.info > coverage/lcov.info + cat coverage/lcov.info - name: Analyze with SonarCloud uses: SonarSource/sonarcloud-github-action@master diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..6254ab7b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,35 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Flutter Integration Test", + "type": "shell", + "command": "flutter", + "args": [ + "drive", + "--target=apps/multichoice/test_driver/integration_test.dart" + ], + "group": "test", + "problemMatcher": [] + }, + { + "label": "Uninstall App", + "type": "shell", + "command": "adb", + "args": [ + "uninstall", + "co.za.zanderkotze.multichoice" + ], + "problemMatcher": [] + }, + { + "label": "Uninstall and Run Integration Test", + "dependsOn": [ + "Uninstall App", + "Flutter Integration Test" + ], + "dependsOrder": "sequence", + "group": "test" + } + ] +} \ No newline at end of file diff --git a/.vscode/test_launch.json b/.vscode/test_launch.json new file mode 100644 index 00000000..ad10d644 --- /dev/null +++ b/.vscode/test_launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "multichoice (integration test)", + "request": "launch", + "type": "dart", + "program": "apps/multichoice/test_driver/integration_test.dart", + "preLaunchTask": "Flutter Integration Test" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d71ebf88 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Set up integration tests + +- Add integration tests +- Setup integration tests +- Add tasks.json file +- Update _build-android-app file to use this ROOT changelog diff --git a/apps/multichoice/CHANGELOG.txt b/apps/multichoice/CHANGELOG.txt index 0e65250a..599202dd 100644 --- a/apps/multichoice/CHANGELOG.txt +++ b/apps/multichoice/CHANGELOG.txt @@ -1,5 +1,10 @@ # CHANGELOG +Setting up Integration Testing: +- Create documentation (refer to `docs/setting-up-integration-tests.md`) +- start cmd /k "run_integration_test.bat %* && call shutdown_emulator.bat && exit" + +--- Version 0.3.0+153: - Setup and add widget tests - Update melos scripts diff --git a/apps/multichoice/integration_test/app_test.dart b/apps/multichoice/integration_test/app_test.dart new file mode 100644 index 00000000..ec78ac44 --- /dev/null +++ b/apps/multichoice/integration_test/app_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:multichoice/app/export.dart'; +import 'package:multichoice/main.dart' as app; +import 'package:multichoice/presentation/shared/widgets/add_widgets/_base.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final keys = WidgetKeys.instance; + + testWidgets('Test counter increment', (WidgetTester tester) async { + app.main(); + // Wait for the app to settle + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Verify a Permission Required dialog appears + expect(find.text('Permission Required'), findsOneWidget); + expect(find.text('Deny'), findsOneWidget); + expect(find.text('Open Settings'), findsOneWidget); + await tester.tap(find.text('Deny')); + await tester.pumpAndSettle(); + + // On Home Screen - Verify Add Tab Card + expect(find.byIcon(Icons.add_outlined), findsOneWidget); + expect(find.byType(AddTabCard), findsOneWidget); + + // Open Settings Drawer - Test Layout Switch + expect(find.byIcon(Icons.settings_outlined), findsOneWidget); + await tester.tap(find.byIcon(Icons.settings_outlined)); + await tester.pumpAndSettle(); + expect(find.text('Horizontal/Vertical Layout'), findsOneWidget); + expect(find.byKey(keys.layoutSwitch), findsOneWidget); + await tester.tap(find.byKey(keys.layoutSwitch)); + await tester.pumpAndSettle(); + expect(find.byIcon(Icons.close_outlined), findsOneWidget); + await tester.tap(find.byIcon(Icons.close_outlined)); + await tester.pumpAndSettle(); + + // On Home Screen - Add New Tab + expect(find.byIcon(Icons.add_outlined), findsOneWidget); + expect(find.byType(AddTabCard), findsOneWidget); + await tester.tap(find.byType(AddTabCard)); + await tester.pumpAndSettle(); + + // Add New Tab Dialog + expect(find.text('Add New Tab'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); + expect(find.text('Add'), findsOneWidget); + + // Enter Tab Data + expect(find.byType(TextFormField), findsExactly(2)); + await tester.enterText(find.byType(TextFormField).first, 'Tab 1'); + await tester.enterText(find.byType(TextFormField).last, 'Tab 2'); + await tester.pumpAndSettle(); + expect(find.text('Tab 1'), findsOneWidget); + expect(find.text('Tab 2'), findsOneWidget); + await tester.tap(find.text('Add')); + await tester.pumpAndSettle(); + + expect(find.text('Tab 1'), findsOneWidget); + + // Open Settings Drawer - Test Light/Dark Mode + await tester.tap(find.byIcon(Icons.settings_outlined)); + await tester.pumpAndSettle(); + expect(find.text('Light / Dark Mode'), findsOneWidget); + expect(find.byKey(keys.lightDarkModeSwitch), findsOneWidget); + await tester.tap(find.byKey(keys.lightDarkModeSwitch)); + await tester.pumpAndSettle(); + expect(find.text('Tab 1'), findsOneWidget); + expect(find.byType(AddTabCard), findsOneWidget); + final BuildContext context = tester.element(find.byType(AddTabCard)); + final theme = Theme.of(context); + expect(theme.brightness, Brightness.dark); + + // On Home Screen + await tester.tap(find.byIcon(Icons.search_outlined)); + await tester.pumpAndSettle(); + expect(find.textContaining('not been implemented'), findsOneWidget); + }); +} diff --git a/apps/multichoice/lib/app/engine/widget_keys.dart b/apps/multichoice/lib/app/engine/widget_keys.dart index 988b2b83..2db2b72b 100644 --- a/apps/multichoice/lib/app/engine/widget_keys.dart +++ b/apps/multichoice/lib/app/engine/widget_keys.dart @@ -7,6 +7,7 @@ class WidgetKeys { final deleteModalTitle = const Key('DeleteModalTitle'); final layoutSwitch = const Key('LayoutSwitch'); + final lightDarkModeSwitch = const Key('LightDarkSwitch'); final addTabSizedBox = const Key('AddTabSizedBox'); } diff --git a/apps/multichoice/lib/presentation/drawer/home_drawer.dart b/apps/multichoice/lib/presentation/drawer/home_drawer.dart index cd88c8d9..68cdb1db 100644 --- a/apps/multichoice/lib/presentation/drawer/home_drawer.dart +++ b/apps/multichoice/lib/presentation/drawer/home_drawer.dart @@ -58,15 +58,13 @@ class HomeDrawer extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), children: [ const _LightDarkModeButton(), - ListTile( + SwitchListTile( + key: context.keys.layoutSwitch, title: const Text('Horizontal/Vertical Layout'), - trailing: Switch( - key: context.keys.layoutSwitch, - value: context.watch().appLayout, - onChanged: (value) { - context.read().appLayout = value; - }, - ), + value: context.watch().appLayout, + onChanged: (value) { + context.read().appLayout = value; + }, ), ListTile( title: const Text('Delete All Data'), diff --git a/apps/multichoice/lib/presentation/drawer/widgets/_light_dark_mode_button.dart b/apps/multichoice/lib/presentation/drawer/widgets/_light_dark_mode_button.dart index 33440297..1c1f2728 100644 --- a/apps/multichoice/lib/presentation/drawer/widgets/_light_dark_mode_button.dart +++ b/apps/multichoice/lib/presentation/drawer/widgets/_light_dark_mode_button.dart @@ -13,6 +13,7 @@ class _LightDarkModeButton extends HookWidget { final isDark = useState(isDarkMode); return SwitchListTile( + key: context.keys.lightDarkModeSwitch, title: const Text('Light / Dark Mode'), value: isDark.value, activeThumbImage: AssetImage(Assets.images.sleepMode.path), diff --git a/apps/multichoice/pubspec.yaml b/apps/multichoice/pubspec.yaml index 26c61688..1b82deb7 100644 --- a/apps/multichoice/pubspec.yaml +++ b/apps/multichoice/pubspec.yaml @@ -2,7 +2,7 @@ name: multichoice description: "The application for the Multichoice repo" publish_to: "none" -version: 0.3.0+158 +version: 0.3.0+163 environment: sdk: ">=3.3.0 <4.0.0" @@ -36,6 +36,8 @@ dev_dependencies: flutter_gen_runner: ^5.5.0+1 flutter_test: sdk: flutter + integration_test: + sdk: flutter very_good_analysis: ^7.0.0 flutter: diff --git a/apps/multichoice/test/presentation/shared/widgets/add_widgets/entry_test.dart b/apps/multichoice/test/presentation/shared/widgets/add_widgets/entry_test.dart index 18fc61d5..535342b1 100644 --- a/apps/multichoice/test/presentation/shared/widgets/add_widgets/entry_test.dart +++ b/apps/multichoice/test/presentation/shared/widgets/add_widgets/entry_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:multichoice/presentation/shared/widgets/add_widgets/_base.dart'; -import '../../../../helpers/widget_wrapper.dart'; +import '../../../../helpers/export.dart'; void main() { testWidgets('AddEntryCard renders correctly and responds to tap', diff --git a/apps/multichoice/test/presentation/shared/widgets/add_widgets/tab_test.dart b/apps/multichoice/test/presentation/shared/widgets/add_widgets/tab_test.dart index 98916668..7ba49d7a 100644 --- a/apps/multichoice/test/presentation/shared/widgets/add_widgets/tab_test.dart +++ b/apps/multichoice/test/presentation/shared/widgets/add_widgets/tab_test.dart @@ -32,13 +32,7 @@ void main() { final cardWidget = tester.widget(find.byType(Card)); expect(cardWidget.color, equals(testColor)); - // debugDumpApp(); final sizedBox = tester.widget( - // find.descendant( - // of: find.byType(Padding), - // matching: find.byType(SizedBox), - // matchRoot: true, - // ), find.byKey(const Key('AddTabSizedBox')), ); diff --git a/apps/multichoice/test/presentation/shared/widgets/modals/delete_modal_test.dart b/apps/multichoice/test/presentation/shared/widgets/modals/delete_modal_test.dart index effec6bc..6738e011 100644 --- a/apps/multichoice/test/presentation/shared/widgets/modals/delete_modal_test.dart +++ b/apps/multichoice/test/presentation/shared/widgets/modals/delete_modal_test.dart @@ -75,9 +75,6 @@ void main() { testWidgets('deleteModal displays correctly and handles actions', (WidgetTester tester) async { var confirmPressed = false; - // Will still possibly be used. - // ignore: unused_local_variable - const cancelPressed = false; await tester.pumpWidget( MaterialApp( diff --git a/apps/multichoice/test_driver/integration_test.dart b/apps/multichoice/test_driver/integration_test.dart new file mode 100644 index 00000000..b38629cc --- /dev/null +++ b/apps/multichoice/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/docs/setting-up-integration-tests.md b/docs/setting-up-integration-tests.md new file mode 100644 index 00000000..891868e2 --- /dev/null +++ b/docs/setting-up-integration-tests.md @@ -0,0 +1,157 @@ +# Flutter Integration Test Automation on Windows (for Android) + +This guide explains how to: + +* Set up Flutter integration testing for a mobile app. +* Automatically detect whether an Android emulator is running. +* Start an emulator if none is running. +* Run integration tests using `flutter drive`. +* Avoid launching desktop/web devices like Chrome, Edge, or Windows. + +--- + +## How to run integration tests + +- Run `apps/multichoice/integration_test/app_test.dart` + +## 1. Prerequisites + +* Flutter SDK installed and properly configured. +* Android Studio installed with at least one virtual device (AVD) created. +* Java installed (used by Android tools). +* A Flutter app with `integration_test` and `test_driver` folders set up. +* Windows system with access to Command Prompt or PowerShell. + +--- + +## 2. File & Folder Structure + +Ensure the following paths exist in your project: + +* `apps/multichoice/test_driver/integration_test.dart` +* `apps/multichoice/integration_test/app_test.dart` + +These are required by your integration test. + +--- + +## 3. Custom Batch Script: `run_integration_test.bat` + +This script: + +* Checks for connected Android devices/emulators. +* Ignores non-mobile devices (like Chrome or Windows). +* Starts the first available emulator if none is running. +* Runs the `flutter drive` test command for integration. + +Place the following script in your project root as `run_integration_test.bat`: + +```batch +@echo off + +:: Check if an Android device or emulator is running +flutter devices | findstr /C:"No devices" >nul +if %errorlevel%==0 ( + echo No devices found. Starting emulator... + + :: Get the first available Android emulator + for /f "tokens=*" %%i in ('emulator -list-avds') do ( + set EMULATOR_NAME=%%i + goto :start_emulator + ) + + echo No emulator found. Please create one in Android Studio. + exit /b 1 + + :start_emulator + echo Starting emulator %EMULATOR_NAME%... + start emulator -avd %EMULATOR_NAME% + + :: Wait for it to be fully online + echo Waiting for emulator to start... + adb wait-for-device +) else ( + :: Check if a mobile (Android) device is connected + flutter devices | findstr /C:"android" >nul + if %errorlevel%==1 ( + echo No Android devices or emulators found. Starting an emulator... + + for /f "tokens=*" %%i in ('emulator -list-avds') do ( + set EMULATOR_NAME=%%i + goto :start_emulator + ) + + echo No emulator found. Please create one in Android Studio. + exit /b 1 + ) else ( + echo Android device or emulator already connected. + ) +) + +:: Run integration test +echo Running Flutter integration test... +flutter drive --driver=apps/multichoice/test_driver/integration_test.dart --target=apps/multichoice/integration_test/app_test.dart +``` + +--- + +## 4. Add Android Emulator to System PATH + +### Problem + +If you get the error: + +```text +'emulator' is not recognized as an internal or external command, +operable program or batch file. +``` + +### Solution + +Add the following paths to your Windows system PATH: + +* `C:\Users\YourUsername\AppData\Local\Android\Sdk\emulator` +* `C:\Users\YourUsername\AppData\Local\Android\Sdk\platform-tools` + +### Steps + +1. Open Control Panel → System → Advanced system settings → Environment Variables. +2. Under System Variables, select `Path` → Edit → Add the above directories. +3. Restart your terminal. + +### Verify + +* Run `emulator -list-avds` to see available emulators. +* Run `adb devices` to confirm devices are connected. + +--- + +## 5. Usage + +Open Command Prompt or PowerShell and run: + +```batch +run_integration_test.bat +``` + +It will: + +* Detect running Android devices or emulators. +* Start one if needed. +* Run your `flutter drive` integration test. + +--- + +## 6. Gotchas + +* Emulator must exist (create one via Android Studio → Tools → AVD Manager). +* If only Chrome, Edge, or Windows devices are detected, they will be ignored. +* Emulators may take time to boot up on first launch. +* Integration tests must be run using `flutter drive`, not `flutter run`. +* This script assumes Android-only testing (not desktop/web). + +--- + +## 7. Optional: VS Code Integration + +You can run the batch script from a VS Code launch configuration using `preLaunchTask`. Ask for setup details if needed. diff --git a/melos.yaml b/melos.yaml index 2a39d89a..96ea31d8 100644 --- a/melos.yaml +++ b/melos.yaml @@ -84,6 +84,22 @@ scripts: flutter: true scope: ["integration"] + coverage:all: + description: | + Run all tests and generate coverage reports. + run: | + melos run coverage:core --no-select && melos run coverage:multichoice --no-select + + coverage:integration: + run: flutter test -j 1 --coverage + exec: + failFast: true + concurrency: 1 + orderDependents: true + packageFilters: + flutter: true + scope: ["integration"] + coverage:core: run: flutter test -j 1 --coverage exec: @@ -92,7 +108,7 @@ scripts: orderDependents: true packageFilters: flutter: true - scope: "core" + scope: ["core"] coverage:multichoice: run: flutter test -j 1 --coverage @@ -102,7 +118,7 @@ scripts: orderDependents: true packageFilters: flutter: true - scope: "core" + scope: ["multichoice"] coverage:multichoice:windows: run: flutter test -j 1 --coverage && perl "C:\\ProgramData\\chocolatey\\lib\\lcov\\tools\\bin\\genhtml" --no-function-coverage -o coverage\html coverage\lcov.info && start coverage\html\index.html diff --git a/packages/core/test/services/database_service_test.dart b/packages/core/test/services/database_service_test.dart new file mode 100644 index 00000000..debf987f --- /dev/null +++ b/packages/core/test/services/database_service_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DatabaseService Tests', () { + test('Singleton instance should be the same', () {}); + + test('Initial database should be empty', () {}); + + test('Adding entries to the database', () {}); + }); +}