Add Fastlane Play Store integration with automated screenshots
Fastlane supply config (android/fastlane) with de-DE metadata structure, integration_test-driven screenshot automation (screenshot_test.dart, test_driver, tool/screenshots.sh) using stable keys on the login form, refreshed materials/screenshots (phone + 7-inch) and app banner/symbol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -350,3 +350,15 @@ hs_err_pid*
|
||||
|
||||
*.idea*
|
||||
**/.DS_store
|
||||
|
||||
# Fastlane (Play-Store-Upload)
|
||||
android/fastlane/report.xml
|
||||
android/fastlane/Preview.html
|
||||
android/fastlane/play-service-account.json
|
||||
android/fastlane/README.md.bak
|
||||
# lokale Screenshots außerhalb der Fastlane-Metadaten
|
||||
/screenshots/
|
||||
# Fastlane-Bild-PNGs sind generierte Kopien; kanonisch/versioniert liegen die
|
||||
# Screenshots unter materials/screenshots/. Nur die leeren Ordner (.gitkeep)
|
||||
# bleiben eingecheckt, damit supply die Struktur vorfindet.
|
||||
android/fastlane/metadata/android/**/images/**/*.png
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package_name("eu.mhsl.marianum.mobile.client")
|
||||
|
||||
# Service-Account-JSON für die Google Play Developer API. Pfad über die
|
||||
# Umgebungsvariable SUPPLY_JSON_KEY setzen (die Datei selbst wird per
|
||||
# .gitignore nicht eingecheckt).
|
||||
json_key_file(ENV["SUPPLY_JSON_KEY"] || "fastlane/play-service-account.json")
|
||||
@@ -0,0 +1,29 @@
|
||||
default_platform(:android)
|
||||
|
||||
# Play-Store-Auslieferung über fastlane supply. Die Screenshots erzeugt
|
||||
# ../tool/screenshots.sh (Demo-Login + integration_test) direkt in
|
||||
# metadata/android/<locale>/images/. Texte/Changelogs liegen daneben in
|
||||
# metadata/android/<locale>/.
|
||||
platform :android do
|
||||
desc "Nur Screenshots hochladen (verändert keine Texte/Binaries)"
|
||||
lane :upload_screenshots do
|
||||
upload_to_play_store(
|
||||
skip_upload_apk: true,
|
||||
skip_upload_aab: true,
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_changelogs: true,
|
||||
skip_upload_images: false,
|
||||
skip_upload_screenshots: false,
|
||||
metadata_path: "./fastlane/metadata/android",
|
||||
)
|
||||
end
|
||||
|
||||
desc "Store-Texte, Changelogs und Screenshots hochladen (keine Binaries)"
|
||||
lane :upload_metadata do
|
||||
upload_to_play_store(
|
||||
skip_upload_apk: true,
|
||||
skip_upload_aab: true,
|
||||
metadata_path: "./fastlane/metadata/android",
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
# fastlane-Plugins (aktuell keine). Datei vorhanden, damit `fastlane` das
|
||||
# Plugin-Handling initialisiert.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Fastlane – Play-Store-Screenshots & Upload
|
||||
|
||||
Schritt 2 der Play-Store-Automatisierung. Die App muss dafür **nicht** neu
|
||||
gebaut werden – der Demo-Modus (`demo@`-Login) liefert die kompletten Inhalte
|
||||
aus Fixtures.
|
||||
|
||||
## 1. Screenshots aufnehmen
|
||||
|
||||
Ein Gerät/Emulator (≥ Android 8) per `adb devices` sichtbar, dann aus dem
|
||||
**Client**-Wurzelverzeichnis:
|
||||
|
||||
```bash
|
||||
tool/screenshots.sh
|
||||
```
|
||||
|
||||
Fährt Phone (1080×1920), 7″ (1200×1920) und 10″ (1600×2560) nacheinander durch
|
||||
(setzt die Displaygröße per `adb shell wm size/density`) und legt die PNGs in
|
||||
`metadata/android/de-DE/images/{phone,sevenInch,tenInch}Screenshots/` ab.
|
||||
|
||||
- Nur eine Größe: `PROFILES="phone" tool/screenshots.sh`
|
||||
- Profile-Build (flüssigere Frames): `BUILD_MODE=profile tool/screenshots.sh`
|
||||
|
||||
## 2. Hochladen
|
||||
|
||||
Service-Account-JSON der Google Play Developer API besorgen und referenzieren:
|
||||
|
||||
```bash
|
||||
export SUPPLY_JSON_KEY=/pfad/zu/play-service-account.json
|
||||
cd android
|
||||
fastlane upload_screenshots # nur Bilder
|
||||
# oder
|
||||
fastlane upload_metadata # Bilder + Texte/Changelogs, keine Binaries
|
||||
```
|
||||
|
||||
Der Service-Account-Key wird **nicht** eingecheckt (siehe `.gitignore`).
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:marianum_mobile/app.dart';
|
||||
import 'package:marianum_mobile/main.dart' as app;
|
||||
import 'package:marianum_mobile/routing/app_routes.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/app_modules.dart';
|
||||
import 'package:marianum_mobile/state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import 'package:marianum_mobile/view/pages/talk/widgets/chat_tile.dart';
|
||||
|
||||
/// Marketing-Screenshot-Lauf für die Stores. Loggt sich über den Demo-Login
|
||||
/// (`demo@…`, siehe DemoMode) ein und nimmt einen kuratierten Satz Screens auf:
|
||||
/// Stundenplan (hell + dunkel), Talk-Liste, Talk-Chat, Dateien, „Mehr"-Bereich
|
||||
/// und Einstellungen. Ausgeführt über `flutter drive` mit
|
||||
/// test_driver/integration_test.dart, orchestriert von tool/screenshots.sh
|
||||
/// (Phone / 7″ / 10″).
|
||||
///
|
||||
/// Kein Netzwerk: der Demo-Modus beantwortet allen Backend-Verkehr aus
|
||||
/// Fixtures, daher ist der Lauf deterministisch.
|
||||
Future<void> main() async {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('Store-Screenshots über den Demo-Login', (tester) async {
|
||||
// Der integration_test-Harness installiert eigene Error-Handler. app()
|
||||
// überschreibt sie in main() durch die produktiven (ClientErrorReporter),
|
||||
// was die End-of-Test-Buchführung des Harness sprengt (_pendingExceptionDetails).
|
||||
// Daher vorher sichern und nach main() zurücksetzen.
|
||||
final harnessFlutterOnError = FlutterError.onError;
|
||||
final harnessDispatcherOnError = PlatformDispatcher.instance.onError;
|
||||
|
||||
_log('main() starten');
|
||||
await app.main();
|
||||
|
||||
FlutterError.onError = harnessFlutterOnError;
|
||||
PlatformDispatcher.instance.onError = harnessDispatcherOnError;
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
_log('Login');
|
||||
await _login(tester);
|
||||
|
||||
_log('warte auf App-Shell');
|
||||
await _pumpUntil(
|
||||
tester,
|
||||
find.byType(App),
|
||||
timeout: const Duration(seconds: 20),
|
||||
);
|
||||
_log('warte auf Post-Login-Splash-Ende');
|
||||
await _pumpUntilGone(
|
||||
tester,
|
||||
find.byKey(const ValueKey('post-login-splash')),
|
||||
timeout: const Duration(seconds: 12),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Screenshots auf Android brauchen eine einmalige Umwandlung der
|
||||
// Flutter-Surface in eine lesbare Image-Texture. Erst hier – nach Login und
|
||||
// Splash –, damit der sichtbare Screen erst kurz vor der Aufnahme „einfriert".
|
||||
if (Platform.isAndroid) {
|
||||
_log('convertFlutterSurfaceToImage');
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
}
|
||||
|
||||
// 1 + 2: Stundenplan in beiden Themes.
|
||||
await _goTo(tester, Modules.timetable);
|
||||
_setTheme(tester, ThemeMode.light);
|
||||
await _capture(tester, binding, '1_stundenplan_hell');
|
||||
_setTheme(tester, ThemeMode.dark);
|
||||
await _capture(tester, binding, '2_stundenplan_dunkel');
|
||||
|
||||
// Restlicher Satz einheitlich im hellen Theme.
|
||||
_setTheme(tester, ThemeMode.light);
|
||||
|
||||
// 3: Talk-Liste.
|
||||
await _goTo(tester, Modules.talk);
|
||||
await _capture(tester, binding, '3_talk_liste');
|
||||
|
||||
// 4: Talk-Chat (ersten Chat der Liste öffnen).
|
||||
await tester.tap(find.byType(ChatTile).first);
|
||||
await _capture(tester, binding, '4_talk_chat');
|
||||
_popPushedPage(tester);
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// 5: Dateien.
|
||||
await _goTo(tester, Modules.files);
|
||||
await _capture(tester, binding, '5_dateien');
|
||||
|
||||
// 6: „Mehr"-Bereich (letzter Tab, hinter den Modul-Tabs).
|
||||
_goToMore(tester);
|
||||
await _capture(tester, binding, '6_mehr');
|
||||
|
||||
// 7: Einstellungen (Vollbild-Push).
|
||||
AppRoutes.openSettings(tester.element(find.byType(App)));
|
||||
await _capture(tester, binding, '7_einstellungen');
|
||||
_popPushedPage(tester);
|
||||
|
||||
_log('fertig');
|
||||
});
|
||||
}
|
||||
|
||||
void _log(String message) => debugPrint('SHOTS: $message');
|
||||
|
||||
Future<void> _login(WidgetTester tester) async {
|
||||
final loginVisible = await _pumpUntil(
|
||||
tester,
|
||||
find.byKey(const Key('login-username-field')),
|
||||
);
|
||||
if (!loginVisible) {
|
||||
_log('kein Login-Screen sichtbar – bereits angemeldet, überspringe Login');
|
||||
return;
|
||||
}
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('login-username-field')),
|
||||
'demo@screenshots',
|
||||
);
|
||||
await tester.enterText(find.byKey(const Key('login-password-field')), 'demo');
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('login-submit-button')));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
/// Setzt das App-Theme über den [SettingsCubit] (MaterialApp folgt
|
||||
/// `settings.appTheme`). `val(write: true)` plant den Emit als Microtask.
|
||||
void _setTheme(WidgetTester tester, ThemeMode mode) {
|
||||
final context = tester.element(find.byType(App));
|
||||
context.read<SettingsCubit>().val(write: true).appTheme = mode;
|
||||
}
|
||||
|
||||
/// Navigiert zu [module] – über den Bottom-Tab, wenn er in der Leiste liegt,
|
||||
/// sonst als Vollbild-Push. Größenunabhängig, da die Tab-Anzahl je Gerät variiert.
|
||||
Future<void> _goTo(WidgetTester tester, Modules module) async {
|
||||
final appFinder = find.byType(App);
|
||||
if (appFinder.evaluate().isEmpty) {
|
||||
_log('WARN: App-Shell fehlt, kann $module nicht ansteuern');
|
||||
return;
|
||||
}
|
||||
final context = tester.element(appFinder);
|
||||
if (!AppRoutes.goToTab(context, module)) {
|
||||
final resolved = AppModule.modules(context)[module];
|
||||
if (resolved != null) AppRoutes.openModule(context, resolved);
|
||||
}
|
||||
}
|
||||
|
||||
/// Springt auf den „Mehr"-Tab. Der liegt hinter den Modul-Tabs, sein Index ist
|
||||
/// also die Anzahl der Bottom-Bar-Module.
|
||||
void _goToMore(WidgetTester tester) {
|
||||
final context = tester.element(find.byType(App));
|
||||
final moreIndex = AppModule.getBottomBarModules(context).length;
|
||||
app.Main.bottomNavigator.jumpToTab(moreIndex);
|
||||
}
|
||||
|
||||
/// Schließt einen per [AppRoutes] gepushten Vollbild-Screen wieder.
|
||||
void _popPushedPage(WidgetTester tester) {
|
||||
final navigator = AppRoutes.rootNavigatorKey.currentState;
|
||||
if (navigator != null && navigator.canPop()) navigator.pop();
|
||||
}
|
||||
|
||||
/// Lässt die Oberfläche zur Ruhe kommen und legt den Screenshot ab.
|
||||
Future<void> _capture(
|
||||
WidgetTester tester,
|
||||
IntegrationTestWidgetsFlutterBinding binding,
|
||||
String name,
|
||||
) async {
|
||||
// Feste Setzzeit statt „warte auf Spinner-Ende": die Demo-Fixtures kommen
|
||||
// synchron (keine künstlichen Delays), aber Module wie Dateien/Chat laden erst
|
||||
// beim Navigieren nach. Eine globale CircularProgressIndicator-Prüfung taugt
|
||||
// nicht, weil die PersistentTabView alle Tabs am Leben hält und
|
||||
// Avatar-Platzhalter dauerhaft einen Spinner zeigen. In 500ms-Schritten
|
||||
// pumpen, damit Timer/Microtasks (u.a. der Theme-Emit) durchlaufen.
|
||||
await _settle(tester, const Duration(seconds: 6));
|
||||
_log('-> $name: takeScreenshot');
|
||||
await binding.takeScreenshot(name);
|
||||
}
|
||||
|
||||
/// Pumpt [duration] in 500ms-Schritten ab, damit die App async Fixtures laden
|
||||
/// und ihre Frames rendern kann (Ersatz für pumpAndSettle, das an den
|
||||
/// Dauer-Animationen hängen bleibt).
|
||||
Future<void> _settle(WidgetTester tester, Duration duration) async {
|
||||
final steps = duration.inMilliseconds ~/ 500;
|
||||
for (var i = 0; i < steps; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
}
|
||||
}
|
||||
|
||||
/// Pumpt in kurzen Schritten, bis [finder] erscheint oder [timeout] abläuft.
|
||||
/// Gibt zurück, ob [finder] gefunden wurde.
|
||||
Future<bool> _pumpUntil(
|
||||
WidgetTester tester,
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final end = DateTime.now().add(timeout);
|
||||
while (DateTime.now().isBefore(end)) {
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
if (finder.evaluate().isNotEmpty) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Gegenstück zu [_pumpUntil]: pumpt, bis [finder] nichts mehr trifft.
|
||||
Future<bool> _pumpUntilGone(
|
||||
WidgetTester tester,
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final end = DateTime.now().add(timeout);
|
||||
while (DateTime.now().isBefore(end)) {
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
if (finder.evaluate().isEmpty) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -111,6 +111,7 @@ class _LoginCardState extends State<LoginCard> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
key: const Key('login-username-field'),
|
||||
controller: _usernameController,
|
||||
enabled: !loading,
|
||||
validator: _required,
|
||||
@@ -125,6 +126,7 @@ class _LoginCardState extends State<LoginCard> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
key: const Key('login-password-field'),
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
enabled: !loading,
|
||||
@@ -146,6 +148,7 @@ class _LoginCardState extends State<LoginCard> {
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: FilledButton(
|
||||
key: const Key('login-submit-button'),
|
||||
onPressed: loading ? null : _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 671 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 219 KiB |
@@ -98,6 +98,11 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
# Screenshot-Automatisierung für den Play Store: treibt den Demo-Login über
|
||||
# flutter drive und ruft binding.takeScreenshot pro Hauptscreen auf
|
||||
# (integration_test/ + test_driver/, orchestriert von tool/screenshots.sh).
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
fake_async: ^1.3.1
|
||||
|
||||
flutter_launcher_icons: ^0.14.3
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:integration_test/integration_test_driver_extended.dart';
|
||||
|
||||
/// Host-seitiger Treiber für den Screenshot-Lauf. Schreibt jeden von
|
||||
/// `binding.takeScreenshot(name)` gelieferten PNG-Frame nach
|
||||
/// `$SCREENSHOT_OUT_DIR/<name>.png` (Default: ./screenshots). Das Zielverzeichnis
|
||||
/// setzt tool/screenshots.sh pro Gerätegröße auf das passende Fastlane-Bildverzeichnis.
|
||||
Future<void> main() async {
|
||||
final outDir = Platform.environment['SCREENSHOT_OUT_DIR'] ?? 'screenshots';
|
||||
|
||||
await integrationDriver(
|
||||
onScreenshot: (name, bytes, [args]) async {
|
||||
final file = File('$outDir/$name.png');
|
||||
await file.create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
stdout.writeln('Screenshot gespeichert: ${file.path}');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Store-Screenshots über den Demo-Login aufnehmen.
|
||||
#
|
||||
# Fährt den integration_test-Screenshot-Lauf (integration_test/screenshot_test.dart)
|
||||
# über flutter drive für die drei von Google Play verlangten Formfaktoren durch
|
||||
# (Phone + 7″ + 10″ Tablet sind inzwischen Pflicht). Statt drei AVDs zu verwalten,
|
||||
# wird EIN laufendes Gerät/Emulator pro Durchlauf per `adb shell wm size/density`
|
||||
# auf die Zielauflösung gebracht; takeScreenshot liefert dann Frames in genau
|
||||
# dieser Pixelgröße.
|
||||
#
|
||||
# Ablage:
|
||||
# - kanonisch (versioniert): materials/screenshots/android/<formfaktor>/
|
||||
# - Kopie für den Upload: android/fastlane/metadata/android/de-DE/images/<...>Screenshots/
|
||||
# Danach: `cd android && fastlane upload_screenshots`.
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - genau ein per `adb devices` sichtbares Gerät/Emulator (>= Android 8)
|
||||
# - flutter, adb im PATH
|
||||
#
|
||||
# Aufruf (vom Client-Projektwurzelverzeichnis):
|
||||
# tool/screenshots.sh # debug-Build (schnell)
|
||||
# BUILD_MODE=profile tool/screenshots.sh
|
||||
# PROFILES="phone" tool/screenshots.sh # nur eine Größe
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
BUILD_MODE="${BUILD_MODE:-debug}"
|
||||
MATERIALS_BASE="materials/screenshots/android"
|
||||
FASTLANE_BASE="android/fastlane/metadata/android/de-DE/images"
|
||||
APP_ID="eu.mhsl.marianum.mobile.client"
|
||||
|
||||
# name | breite x höhe (px) | dichte (dpi) | materials-Ordner | fastlane-Ordner
|
||||
# Phone nutzt die NATIVE Displayauflösung des Geräts (voller Hochformat, nicht
|
||||
# auf 16:9 gestaucht). Tablets im QUERFORMAT (Breite > Höhe); bei den
|
||||
# Landscape-Breiten (>1000dp) zeigt Talk zudem die Master-Detail-Splitview.
|
||||
# Dichten so gewählt, dass die adaptive Bottom-Bar die passende Tab-Anzahl zeigt.
|
||||
declare -A PROFILE_SIZE=(
|
||||
[phone]="native" [seven]="1920x1200" [ten]="2560x1600"
|
||||
)
|
||||
declare -A PROFILE_DENSITY=(
|
||||
[phone]="native" [seven]="240" [ten]="280"
|
||||
)
|
||||
declare -A PROFILE_MATERIALS=(
|
||||
[phone]="phone" [seven]="7-inch" [ten]="10-inch"
|
||||
)
|
||||
declare -A PROFILE_FASTLANE=(
|
||||
[phone]="phoneScreenshots"
|
||||
[seven]="sevenInchScreenshots"
|
||||
[ten]="tenInchScreenshots"
|
||||
)
|
||||
|
||||
PROFILES="${PROFILES:-phone seven ten}"
|
||||
|
||||
# App-APK einmal vorab bauen, damit sie vor jedem `flutter drive` installiert
|
||||
# und die Notification-Permission gegrantet werden kann (drive deinstalliert die
|
||||
# App nach jedem Lauf, sonst wäre der Grant weg und ein Systemdialog könnte die
|
||||
# Screenshots verdecken). Der eigentliche drive-Lauf reinstalliert sie per -r.
|
||||
APK="build/app/outputs/flutter-apk/app-${BUILD_MODE}.apk"
|
||||
echo "Baue App-APK ($BUILD_MODE) vorab…"
|
||||
flutter build apk "--$BUILD_MODE"
|
||||
|
||||
reset_display() {
|
||||
echo "Setze Displaygröße/-dichte zurück…"
|
||||
adb shell wm size reset || true
|
||||
adb shell wm density reset || true
|
||||
}
|
||||
trap reset_display EXIT
|
||||
|
||||
for profile in $PROFILES; do
|
||||
size="${PROFILE_SIZE[$profile]}"
|
||||
density="${PROFILE_DENSITY[$profile]}"
|
||||
materials_dir="$MATERIALS_BASE/${PROFILE_MATERIALS[$profile]}"
|
||||
fastlane_dir="$FASTLANE_BASE/${PROFILE_FASTLANE[$profile]}"
|
||||
|
||||
echo ""
|
||||
echo "=== Profil '$profile' ($size @ ${density}dpi) -> $materials_dir ==="
|
||||
mkdir -p "$materials_dir" "$fastlane_dir"
|
||||
rm -f "$materials_dir"/*.png "$fastlane_dir"/*.png
|
||||
|
||||
# "native" -> Geräteauflösung/-dichte verwenden (für Phone), sonst erzwingen.
|
||||
if [ "$size" = "native" ]; then
|
||||
adb shell wm size reset
|
||||
adb shell wm density reset
|
||||
else
|
||||
adb shell wm size "$size"
|
||||
adb shell wm density "$density"
|
||||
fi
|
||||
|
||||
# App sicherstellen, frische (ausgeloggte) Session erzwingen und
|
||||
# Notification-Permission erteilen, bevor drive läuft. pm clear setzt auch die
|
||||
# Runtime-Permissions zurück, daher der Grant DANACH.
|
||||
[ -f "$APK" ] && adb install -r "$APK" >/dev/null 2>&1 || true
|
||||
adb shell pm clear "$APP_ID" >/dev/null 2>&1 || true
|
||||
adb shell pm grant "$APP_ID" android.permission.POST_NOTIFICATIONS 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
SCREENSHOT_OUT_DIR="$materials_dir" flutter drive \
|
||||
--driver=test_driver/integration_test.dart \
|
||||
--target=integration_test/screenshot_test.dart \
|
||||
--"$BUILD_MODE" \
|
||||
--no-dds
|
||||
|
||||
# Kanonische Bilder in den Fastlane-Upload-Ordner spiegeln.
|
||||
cp "$materials_dir"/*.png "$fastlane_dir"/
|
||||
echo "Profil '$profile' fertig: $(ls -1 "$materials_dir"/*.png 2>/dev/null | wc -l) Screenshots"
|
||||
done
|
||||
|
||||
reset_display
|
||||
trap - EXIT
|
||||
|
||||
echo ""
|
||||
echo "Kanonische Screenshots: $MATERIALS_BASE/{phone,7-inch,10-inch}/"
|
||||
echo "Upload: cd android && fastlane upload_screenshots"
|
||||