diff --git a/.github/actions/screenshot-android/action.yml b/.github/actions/screenshot-android/action.yml new file mode 100644 index 000000000..477d3475e --- /dev/null +++ b/.github/actions/screenshot-android/action.yml @@ -0,0 +1,67 @@ +name: "Android Screenshots Workflow" + +inputs: + ANDROID_EMULATOR_API: + description: 'Emulator API to be used when running tests' + required: false + default: 34 + ANDROID_EMULATOR_ARCH: + description: 'Emulator architecture to be used when running tests' + required: false + default: x86_64 + +runs: + using: "composite" + steps: + - name: Enable KVM group perms + shell: bash + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Cache AVD + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ inputs.ANDROID_EMULATOR_API }}-${{ inputs.ANDROID_EMULATOR_ARCH }} + + - name: Create AVD and Cache Snapshot + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ inputs.ANDROID_EMULATOR_API }} + arch: ${{ inputs.ANDROID_EMULATOR_ARCH }} + profile: pixel_6 + avd-name: pixel_6 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + cache: true + + - name: Create Android Screenshots + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ inputs.ANDROID_EMULATOR_API }} + arch: ${{ inputs.ANDROID_EMULATOR_ARCH }} + profile: pixel_6 + avd-name: pixel_6 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: | + flutter drive --driver=test_integration/test_driver.dart --target=test_integration/screenshots.dart -d emulator + + - name: Upload Screenshots + uses: actions/upload-artifact@v4 + with: + name: Android Screenshots + path: screenshots/* diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2cba5a349..116d80f8a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,9 +1,16 @@ name: Badge Magic PR CI + on: pull_request: branches: ["flutter_app"] + +env: + ANDROID_EMULATOR_API: 34 + ANDROID_EMULATOR_ARCH: x86_64 + jobs: common: + name: Common Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,7 +28,6 @@ jobs: - name: Android Workflow uses: ./.github/actions/android - ios: name: iOS Flutter Build needs: common @@ -31,4 +37,16 @@ jobs: - name: iOS Workflow uses: ./.github/actions/ios - \ No newline at end of file + + screenshots: + name: Screenshots (Android) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Android Screenshot Workflow + uses: ./.github/actions/screenshot-android + with: + ANDROID_EMULATOR_API: ${{ env.ANDROID_EMULATOR_API }} + ANDROID_EMULATOR_ARCH: ${{ env.ANDROID_EMULATOR_ARCH }} + \ No newline at end of file diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 22374329a..5d2d9d5ed 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,15 +1,20 @@ name: Badge Magic Push CI + on: push: branches: ["flutter_app"] + +env: + ANDROID_EMULATOR_API: 34 + ANDROID_EMULATOR_ARCH: x86_64 + jobs: common: + name: Common Build runs-on: ubuntu-latest - outputs: VERSION_NAME: ${{ steps.flutter-version.outputs.VERSION_NAME }} VERSION_CODE: ${{ steps.flutter-version.outputs.VERSION_CODE }} - steps: - uses: actions/checkout@v4 @@ -166,6 +171,7 @@ jobs: VERSION_CODE: ${{needs.common.outputs.VERSION_CODE}} update-release: + name: Update Draft Release needs: [common, android, ios] runs-on: ubuntu-latest steps: @@ -175,4 +181,15 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: version: ${{ needs.common.outputs.VERSION_NAME }} - \ No newline at end of file + + screenshots: + name: Screenshots (Android) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Android Screenshot Workflow + uses: ./.github/actions/screenshot-android + with: + ANDROID_EMULATOR_API: ${{ env.ANDROID_EMULATOR_API }} + ANDROID_EMULATOR_ARCH: ${{ env.ANDROID_EMULATOR_ARCH }} diff --git a/.gitignore b/.gitignore index 29a3a5017..f93043f44 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +screenshots/ \ No newline at end of file diff --git a/iOS/Podfile.lock b/iOS/Podfile.lock index 519626f57..74bf99026 100644 --- a/iOS/Podfile.lock +++ b/iOS/Podfile.lock @@ -2,20 +2,39 @@ PODS: - Flutter (1.0.0) - flutter_blue_plus (0.0.1): - Flutter + - fluttertoast (0.0.2): + - Flutter + - Toast + - integration_test (0.0.1): + - Flutter + - Toast (4.1.1) DEPENDENCIES: - Flutter (from `Flutter`) - flutter_blue_plus (from `.symlinks/plugins/flutter_blue_plus/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +SPEC REPOS: + trunk: + - Toast EXTERNAL SOURCES: Flutter: :path: Flutter flutter_blue_plus: :path: ".symlinks/plugins/flutter_blue_plus/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_blue_plus: 4837da7d00cf5d441fdd6635b3a57f936778ea96 + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 + Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/iOS/Runner/AppDelegate.swift b/iOS/Runner/AppDelegate.swift index 70693e4a8..5a92cc91d 100644 --- a/iOS/Runner/AppDelegate.swift +++ b/iOS/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Flutter +import integration_test @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -8,6 +9,9 @@ import Flutter didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) + + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} +} \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index fa489fc6b..6085138cb 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,3 +1,5 @@ +const homeScreenTitleKey = "bm_hm_title"; + //path to all the animation assets used const String animation = 'assets/animations/ic_anim_animation.gif'; const String aniLeft = 'assets/animations/ic_anim_left.gif'; diff --git a/lib/view/homescreen.dart b/lib/view/homescreen.dart index 9cef9920e..61d12a712 100644 --- a/lib/view/homescreen.dart +++ b/lib/view/homescreen.dart @@ -1,3 +1,4 @@ +import 'package:badgemagic/constants.dart'; import 'package:badgemagic/providers/badge_message_provider.dart'; import 'package:badgemagic/providers/cardsprovider.dart'; import 'package:badgemagic/view/widgets/homescreentabs.dart'; @@ -74,6 +75,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { appBar: AppBar( backgroundColor: Colors.red, title: const Text( + key: Key(homeScreenTitleKey), 'Badge Magic', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), @@ -81,92 +83,95 @@ class _HomeScreenState extends State with TickerProviderStateMixin { ), body: SafeArea( child: SizedBox( - height: height, - width: width, - child: Column( - children: [ - const BMBadge(), - Container( - margin: const EdgeInsets.all(15), - child: Material( - borderRadius: BorderRadius.circular(10), - elevation: 10, - child: TextField( - controller: cardData.getController(), - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10)), - prefixIcon: const Icon(Icons.tag_faces_outlined), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.red))), - ), - ), - ), - TabBar( - indicatorSize: TabBarIndicatorSize.label, - controller: _tabController, - tabs: const [ - Tab(text: 'Speed'), - Tab(text: 'Animation'), - Tab(text: 'Effects'), - ], - ), - SingleChildScrollView( - child: Column( - children: [ - AspectRatio( - aspectRatio: 1.5, - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: _tabController, - children: const [ - RadialDial(), - AnimationTab(), - EffectTab(), - ], + height: height, + width: width, + child: SingleChildScrollView( + child: Column( + children: [ + const BMBadge(), + Container( + margin: const EdgeInsets.all(15), + child: Material( + borderRadius: BorderRadius.circular(10), + elevation: 10, + child: TextField( + controller: cardData.getController(), + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + prefixIcon: const Icon(Icons.tag_faces_outlined), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.red))), ), ), - Container( - padding: EdgeInsets.only( - bottom: height * 0.2, - top: height * 0.02), // Adjust the value as needed - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: () { - if (cardData.getController().text.isEmpty) { - Fluttertoast.showToast( - msg: "Please enter some text to transfer.", - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - backgroundColor: Colors.red, - textColor: Colors.white, - fontSize: 16.0, - ); - return; - } - transferData(context, cardData); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.grey.shade400, + ), + TabBar( + indicatorSize: TabBarIndicatorSize.label, + controller: _tabController, + tabs: const [ + Tab(text: 'Speed'), + Tab(text: 'Animation'), + Tab(text: 'Effects'), + ], + ), + SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 1.5, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + controller: _tabController, + children: const [ + RadialDial(), + AnimationTab(), + EffectTab(), + ], + ), + ), + Container( + padding: EdgeInsets.only( + bottom: height * 0.2, + top: height * + 0.02), // Adjust the value as needed + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (cardData.getController().text.isEmpty) { + Fluttertoast.showToast( + msg: + "Please enter some text to transfer.", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + return; + } + transferData(context, cardData); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.grey.shade400, + ), + child: const Text('Transfer'), + ), ), - child: const Text('Transfer'), - ), + ], ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), - ], - ), - ), + )), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index 228f0bacd..9728a1139 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -122,10 +122,15 @@ packages: dependency: "direct main" description: name: flutter_blue_plus - sha256: c762a694c2f67b1f492ef19ead2a30ed3254650bafd852cb8933823d13d7c89f + sha256: ce8241302bf955bfa885457aa571cc215c10444e0c75c3e55d90b5fc05cc7e93 url: "https://pub.dev" source: hosted - version: "1.32.7" + version: "1.32.8" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -160,6 +165,11 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.6" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -184,30 +194,35 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -244,10 +259,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" nested: dependency: transitive description: @@ -288,6 +303,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" provider: dependency: "direct main" description: @@ -341,6 +372,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -353,10 +392,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" typed_data: dependency: transitive description: @@ -401,10 +440,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -421,6 +460,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" xml: dependency: transitive description: @@ -439,4 +486,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.4 <4.0.0" - flutter: ">=3.7.0-0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index f3d5395af..8689a46aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,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_integration/screenshots.dart b/test_integration/screenshots.dart new file mode 100644 index 000000000..6f2e32df0 --- /dev/null +++ b/test_integration/screenshots.dart @@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:badgemagic/main.dart' as app; +import 'package:badgemagic/constants.dart'; +import 'utils.dart'; + +void main() async { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + return Future(() async { + WidgetsApp.debugAllowBannerOverride = false; // Hide the debug banner + if (Platform.isAndroid) { + await binding.convertFlutterSurfaceToImage(); + } + }); + }); + + group('E2E Group', () { + testWidgets('Take Screenshots', (tester) async { + app.main(); + + final homeScreenTitle = find.byKey(const ValueKey(homeScreenTitleKey)); + + await pumpUntilFound(tester, homeScreenTitle); + await tester.pump(const Duration(seconds: 10)); + await binding.takeScreenshot('01'); + }); + }); +} diff --git a/test_integration/test_driver.dart b/test_integration/test_driver.dart new file mode 100644 index 000000000..96a2af453 --- /dev/null +++ b/test_integration/test_driver.dart @@ -0,0 +1,19 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'package:integration_test/integration_test_driver_extended.dart'; + +Future main() async { + await integrationDriver( + onScreenshot: (String screenshotName, List screenshotBytes, + [Map? args]) async { + final filePath = 'screenshots/$screenshotName.png'; + print('Writing screenshot to $filePath'); + + final File image = await File(filePath).create(recursive: true); + image.writeAsBytesSync(screenshotBytes); + return true; + }, + ); +} diff --git a/test_integration/utils.dart b/test_integration/utils.dart new file mode 100644 index 000000000..bfc1b061b --- /dev/null +++ b/test_integration/utils.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; + +Future pumpUntilFound( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 10), +}) async { + bool timerDone = false; + final timer = Timer(timeout, () => timerDone = true); + while (timerDone != true) { + await tester.pump(); + + final found = tester.any(finder); + if (found) { + timerDone = true; + } + } + timer.cancel(); +}