diff --git a/.github/actions/cache-xcode-cas/action.yml b/.github/actions/cache-xcode-cas/action.yml deleted file mode 100644 index 630e57906..000000000 --- a/.github/actions/cache-xcode-cas/action.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: 'Cache Xcode CAS' -description: 'Persist Xcode compilation cache (LLVM CAS) across CI runs' -inputs: - key-suffix: - description: 'Cache key suffix (e.g. runner.os or iOS version from matrix)' - required: true - xcode-version: - description: 'Xcode version for the cache key' - required: false - default: '26.2' -runs: - using: "composite" - steps: - - shell: bash - run: | - echo "FLUTTER_XCODE_COMPILATION_CACHE_ENABLE_CACHING=YES" >> "$GITHUB_ENV" - echo "FLUTTER_XCODE_SWIFT_ENABLE_COMPILE_CACHE=YES" >> "$GITHUB_ENV" - echo "FLUTTER_XCODE_CLANG_ENABLE_COMPILE_CACHE=YES" >> "$GITHUB_ENV" - echo "FLUTTER_XCODE_COMPILATION_CACHE_CAS_PATH=$HOME/xcode-cas" >> "$GITHUB_ENV" - mkdir -p "$HOME/xcode-cas" - - uses: actions/cache@v6 - with: - path: ~/xcode-cas - key: xcode-cas-${{ inputs.xcode-version }}-${{ inputs.key-suffix }}-${{ hashFiles('sample_app/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/ios/Runner.xcodeproj/project.pbxproj') }} - restore-keys: xcode-cas-${{ inputs.xcode-version }}-${{ inputs.key-suffix }}- diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 41e119955..a81421f7b 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -1,4 +1,4 @@ -name: e2e +name: E2E Tests on: workflow_dispatch: @@ -85,14 +85,8 @@ jobs: - uses: ./.github/actions/setup-ruby - - uses: ./.github/actions/cache-cocoapods - with: - key-suffix: ${{ runner.os }} - - - uses: ./.github/actions/cache-xcode-cas - with: - key-suffix: ${{ runner.os }} - xcode-version: ${{ env.IOS_SIMULATOR_VERSION }} + - name: Use Firebase stubs + run: cp sample_app/pubspec_overrides.e2e.yaml sample_app/pubspec_overrides.yaml - name: Bootstrap run: | @@ -101,6 +95,11 @@ jobs: flutter pub global activate melos melos bootstrap + # After Bootstrap so the cache keys hash the resolved pubspec.lock + - uses: ./.github/actions/cache-cocoapods + with: + key-suffix: ${{ runner.os }} + - name: Launch Allure TestOps working-directory: sample_app/ios run: bundle exec fastlane allure_launch diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 4ddd07b66..ad2b6e6cf 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -111,14 +111,8 @@ jobs: version: ${{ matrix.ios }} device: ${{ matrix.device }} - - uses: ./.github/actions/cache-cocoapods - with: - key-suffix: ${{ matrix.ios }} - - - uses: ./.github/actions/cache-xcode-cas - with: - key-suffix: ${{ matrix.ios }} - xcode-version: ${{ env.XCODE_VERSION }} + - name: Use Firebase stubs + run: cp sample_app/pubspec_overrides.e2e.yaml sample_app/pubspec_overrides.yaml - name: Bootstrap run: | @@ -127,6 +121,11 @@ jobs: flutter pub global activate melos melos bootstrap + # After Bootstrap so the cache keys hash the resolved pubspec.lock + - uses: ./.github/actions/cache-cocoapods + with: + key-suffix: ${{ matrix.ios }} + - name: Launch Allure TestOps working-directory: sample_app/ios run: bundle exec fastlane allure_launch cron:true diff --git a/sample_app/e2e_stubs/firebase_core/lib/firebase_core.dart b/sample_app/e2e_stubs/firebase_core/lib/firebase_core.dart new file mode 100644 index 000000000..bde97276a --- /dev/null +++ b/sample_app/e2e_stubs/firebase_core/lib/firebase_core.dart @@ -0,0 +1,55 @@ +/// No-op stand-in for `firebase_core`, swapped in via `pubspec_overrides.e2e.yaml`. +/// +/// Being a pure-Dart package (no `flutter.plugin` section), it removes the Firebase +/// pods / Gradle dependencies from e2e builds entirely: nothing is downloaded, +/// compiled, or initialized. Only the API surface used by `sample_app` is stubbed. +library; + +/// No-op replacement for the Firebase entry point. +class Firebase { + /// Does nothing and returns a dummy [FirebaseApp]. + static Future initializeApp({String? name, FirebaseOptions? options}) async => const FirebaseApp(); +} + +/// Inert replacement for a configured Firebase app. +class FirebaseApp { + /// Creates the inert app handle. + const FirebaseApp(); +} + +/// Plain data holder matching the real `FirebaseOptions` constructor, so the +/// FlutterFire-generated `firebase_options.dart` compiles unchanged. +class FirebaseOptions { + /// Mirrors the real constructor's signature; values are never read. + const FirebaseOptions({ + required this.apiKey, + required this.appId, + required this.messagingSenderId, + required this.projectId, + this.authDomain, + this.databaseURL, + this.storageBucket, + this.measurementId, + this.trackingId, + this.deepLinkURLScheme, + this.androidClientId, + this.iosClientId, + this.iosBundleId, + this.appGroupId, + }); + + /// See the real `firebase_core` for field semantics; all unused here. + final String apiKey, appId, messagingSenderId, projectId; + + /// Optional platform-specific fields accepted for constructor compatibility. + final String? authDomain, + databaseURL, + storageBucket, + measurementId, + trackingId, + deepLinkURLScheme, + androidClientId, + iosClientId, + iosBundleId, + appGroupId; +} diff --git a/sample_app/e2e_stubs/firebase_core/pubspec.yaml b/sample_app/e2e_stubs/firebase_core/pubspec.yaml new file mode 100644 index 000000000..856088105 --- /dev/null +++ b/sample_app/e2e_stubs/firebase_core/pubspec.yaml @@ -0,0 +1,7 @@ +name: firebase_core +description: No-op pure-Dart stand-in for firebase_core, swapped in by e2e builds to keep the native Firebase SDK out of the app. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.0.0 diff --git a/sample_app/e2e_stubs/firebase_crashlytics/lib/firebase_crashlytics.dart b/sample_app/e2e_stubs/firebase_crashlytics/lib/firebase_crashlytics.dart new file mode 100644 index 000000000..b96f25a1c --- /dev/null +++ b/sample_app/e2e_stubs/firebase_crashlytics/lib/firebase_crashlytics.dart @@ -0,0 +1,32 @@ +/// No-op stand-in for `firebase_crashlytics`, swapped in via `pubspec_overrides.e2e.yaml`. +/// +/// See the `firebase_core` stub for the rationale. Only the API surface used by +/// `sample_app` is stubbed; every call silently does nothing. +library; + +import 'package:flutter/foundation.dart'; + +/// No-op replacement for the Crashlytics reporter. +class FirebaseCrashlytics { + FirebaseCrashlytics._(); + + /// The shared no-op instance. + static final FirebaseCrashlytics instance = FirebaseCrashlytics._(); + + /// Does nothing. The positional bool must match the real API's signature. + // ignore: avoid_positional_boolean_parameters + Future setCrashlyticsCollectionEnabled(bool enabled) async {} + + /// Does nothing. + Future recordError( + dynamic exception, + StackTrace? stack, { + dynamic reason, + Iterable information = const [], + bool? printDetails, + bool fatal = false, + }) async {} + + /// Does nothing. + Future recordFlutterFatalError(FlutterErrorDetails flutterErrorDetails) async {} +} diff --git a/sample_app/e2e_stubs/firebase_crashlytics/pubspec.yaml b/sample_app/e2e_stubs/firebase_crashlytics/pubspec.yaml new file mode 100644 index 000000000..bafb8ff01 --- /dev/null +++ b/sample_app/e2e_stubs/firebase_crashlytics/pubspec.yaml @@ -0,0 +1,11 @@ +name: firebase_crashlytics +description: No-op pure-Dart stand-in for firebase_crashlytics, swapped in by e2e builds to keep the native Firebase SDK out of the app. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.0.0 + +dependencies: + flutter: + sdk: flutter diff --git a/sample_app/e2e_stubs/firebase_messaging/lib/firebase_messaging.dart b/sample_app/e2e_stubs/firebase_messaging/lib/firebase_messaging.dart new file mode 100644 index 000000000..f38097fd6 --- /dev/null +++ b/sample_app/e2e_stubs/firebase_messaging/lib/firebase_messaging.dart @@ -0,0 +1,90 @@ +/// No-op stand-in for `firebase_messaging`, swapped in via `pubspec_overrides.e2e.yaml`. +/// +/// See the `firebase_core` stub for the rationale. Only the API surface used by +/// `sample_app` is stubbed: permission requests report denied, token getters +/// return null, and the message streams never emit. +library; + +/// Signature of the top-level FCM background handler. +typedef BackgroundMessageHandler = Future Function(RemoteMessage message); + +/// No-op replacement for the FCM entry point. +class FirebaseMessaging { + FirebaseMessaging._(); + + /// The shared no-op instance. + static final FirebaseMessaging instance = FirebaseMessaging._(); + + /// Never emits. + static Stream get onMessage => const Stream.empty(); + + /// Never emits. + static Stream get onMessageOpenedApp => const Stream.empty(); + + /// Does nothing; [handler] is never invoked. + static void onBackgroundMessage(BackgroundMessageHandler handler) {} + + /// Never emits. + Stream get onTokenRefresh => const Stream.empty(); + + /// Always resolves to null. + Future getToken({String? vapidKey}) async => null; + + /// Always resolves to null. + Future getAPNSToken() async => null; + + /// Always resolves to null. + Future getInitialMessage() async => null; + + /// Always reports [AuthorizationStatus.denied]. + Future requestPermission({ + bool alert = true, + bool announcement = false, + bool badge = true, + bool carPlay = false, + bool criticalAlert = false, + bool provisional = false, + bool providesAppNotificationSettings = false, + bool sound = true, + }) async => const NotificationSettings(authorizationStatus: AuthorizationStatus.denied); + + /// Does nothing. + Future setForegroundNotificationPresentationOptions({ + bool alert = false, + bool badge = false, + bool sound = false, + }) async {} +} + +/// Mirrors the real enum values. +enum AuthorizationStatus { + /// Permission granted. + authorized, + + /// Permission denied — the stub's fixed answer. + denied, + + /// Permission not requested yet. + notDetermined, + + /// Provisional (quiet) permission. + provisional, +} + +/// Minimal result type for [FirebaseMessaging.requestPermission]. +class NotificationSettings { + /// Creates settings with the given [authorizationStatus]. + const NotificationSettings({required this.authorizationStatus}); + + /// The reported permission state. + final AuthorizationStatus authorizationStatus; +} + +/// Minimal FCM message: only [data] is read by `sample_app`. +class RemoteMessage { + /// Creates a message carrying [data]. + const RemoteMessage({this.data = const {}}); + + /// The message payload. + final Map data; +} diff --git a/sample_app/e2e_stubs/firebase_messaging/pubspec.yaml b/sample_app/e2e_stubs/firebase_messaging/pubspec.yaml new file mode 100644 index 000000000..63d1fe314 --- /dev/null +++ b/sample_app/e2e_stubs/firebase_messaging/pubspec.yaml @@ -0,0 +1,7 @@ +name: firebase_messaging +description: No-op pure-Dart stand-in for firebase_messaging, swapped in by e2e builds to keep the native Firebase SDK out of the app. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.0.0 diff --git a/sample_app/pubspec_overrides.e2e.yaml b/sample_app/pubspec_overrides.e2e.yaml new file mode 100644 index 000000000..9137cc18f --- /dev/null +++ b/sample_app/pubspec_overrides.e2e.yaml @@ -0,0 +1,12 @@ +# Copied over pubspec_overrides.yaml by the e2e CI jobs BEFORE `melos bootstrap` +# (melos merges its own managed overrides into the existing file, preserving these +# entries). Swaps the Firebase plugins for the pure-Dart no-op stubs in +# e2e_stubs/ so no native Firebase SDK is downloaded, compiled, or +# initialized during e2e runs. +dependency_overrides: + firebase_core: + path: e2e_stubs/firebase_core + firebase_crashlytics: + path: e2e_stubs/firebase_crashlytics + firebase_messaging: + path: e2e_stubs/firebase_messaging