added automated App Store screenshots via iOS simulators and fastlane deliver
tool/screenshots_ios.sh runs the existing demo-login integration test on iPhone 6.5" (iPhone 14 Plus) and iPad 13" simulators, stores the canonical PNGs under materials/screenshots and mirrors alpha-free JPEGs to ios/fastlane/screenshots for `fastlane upload_screenshots`. Fixed the demo router answering timetable/custom-events with a week, which crashed the inline iOS widget refresh after a demo login. The screenshot test now skips the one-time Talk notification dialog and waits longer for the app shell on cold simulators. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@@ -362,3 +362,9 @@ android/fastlane/README.md.bak
|
||||
# Screenshots unter materials/screenshots/. Nur die leeren Ordner (.gitkeep)
|
||||
# bleiben eingecheckt, damit supply die Struktur vorfindet.
|
||||
android/fastlane/metadata/android/**/images/**/*.png
|
||||
|
||||
# Fastlane (App-Store-Upload)
|
||||
ios/fastlane/report.xml
|
||||
ios/fastlane/Preview.html
|
||||
ios/fastlane/app-store-connect-api-key.json
|
||||
ios/fastlane/screenshots/**/*.jpg
|
||||
|
||||
@@ -17,7 +17,8 @@ import 'package:marianum_mobile/view/pages/talk/widgets/chat_tile.dart';
|
||||
/// 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″).
|
||||
/// (Android: Phone / 7″ / 10″) bzw. tool/screenshots_ios.sh (iPhone 6,5″ /
|
||||
/// iPad 13″).
|
||||
///
|
||||
/// Kein Netzwerk: der Demo-Modus beantwortet allen Backend-Verkehr aus
|
||||
/// Fixtures, daher ist der Lauf deterministisch.
|
||||
@@ -42,11 +43,13 @@ Future<void> main() async {
|
||||
_log('Login');
|
||||
await _login(tester);
|
||||
|
||||
// Großzügig: auf einem frisch angelegten iOS-Simulator dauert der erste
|
||||
// Post-Login-Aufbau deutlich länger als 20s.
|
||||
_log('warte auf App-Shell');
|
||||
await _pumpUntil(
|
||||
tester,
|
||||
find.byType(App),
|
||||
timeout: const Duration(seconds: 20),
|
||||
timeout: const Duration(seconds: 60),
|
||||
);
|
||||
_log('warte auf Post-Login-Splash-Ende');
|
||||
await _pumpUntilGone(
|
||||
@@ -64,6 +67,12 @@ Future<void> main() async {
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
}
|
||||
|
||||
// Den einmaligen Benachrichtigungs-Dialog beim ersten Talk-Besuch
|
||||
// unterdrücken. Auf Android erteilt tool/screenshots.sh die Permission
|
||||
// vorab, auf iOS lässt sie sich im Simulator nicht vorab erteilen – der
|
||||
// Dialog würde sonst über der Talk-Liste liegen.
|
||||
_markTalkPermissionPromptShown(tester);
|
||||
|
||||
// 1 + 2: Stundenplan in beiden Themes.
|
||||
await _goTo(tester, Modules.timetable);
|
||||
_setTheme(tester, ThemeMode.light);
|
||||
@@ -129,6 +138,16 @@ void _setTheme(WidgetTester tester, ThemeMode mode) {
|
||||
context.read<SettingsCubit>().val(write: true).appTheme = mode;
|
||||
}
|
||||
|
||||
void _markTalkPermissionPromptShown(WidgetTester tester) {
|
||||
final context = tester.element(find.byType(App));
|
||||
context
|
||||
.read<SettingsCubit>()
|
||||
.val(write: true)
|
||||
.notificationSettings
|
||||
.talkPermissionPromptShown =
|
||||
true;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
app_identifier("eu.mhsl.marianum.mobile.client")
|
||||
team_id("MY55VF3KPG")
|
||||
@@ -0,0 +1,27 @@
|
||||
default_platform(:ios)
|
||||
|
||||
# App-Store-Auslieferung über fastlane deliver. Die Screenshots erzeugt
|
||||
# ../tool/screenshots_ios.sh (Demo-Login + integration_test) direkt in
|
||||
# screenshots/de-DE/; deliver ordnet sie anhand der Pixelgröße iPhone 6,5″ bzw.
|
||||
# iPad 13″ zu.
|
||||
#
|
||||
# Authentifizierung über einen App-Store-Connect-API-Key (JSON im fastlane-
|
||||
# Format, siehe README.md). Pfad per APP_STORE_CONNECT_API_KEY setzen; die Datei
|
||||
# selbst wird per .gitignore nicht eingecheckt.
|
||||
platform :ios do
|
||||
desc "Nur Screenshots hochladen (ersetzt die vorhandenen, verändert keine Texte/Binaries)"
|
||||
lane :upload_screenshots do
|
||||
upload_to_app_store(
|
||||
api_key_path: ENV["APP_STORE_CONNECT_API_KEY"] || "fastlane/app-store-connect-api-key.json",
|
||||
skip_binary_upload: true,
|
||||
skip_metadata: true,
|
||||
skip_app_version_update: true,
|
||||
skip_screenshots: false,
|
||||
overwrite_screenshots: true,
|
||||
screenshots_path: "./fastlane/screenshots",
|
||||
precheck_include_in_app_purchases: false,
|
||||
run_precheck_before_submit: false,
|
||||
force: true, # kein HTML-Vorschau-Bestätigungsschritt
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# Fastlane – App-Store-Screenshots & Upload
|
||||
|
||||
iOS-Gegenstück zu `android/fastlane`. Die App muss dafür **nicht** neu gebaut
|
||||
werden – der Demo-Modus (`demo@`-Login) liefert die kompletten Inhalte aus
|
||||
Fixtures.
|
||||
|
||||
## 1. Screenshots aufnehmen
|
||||
|
||||
Aus dem **Client**-Wurzelverzeichnis:
|
||||
|
||||
```bash
|
||||
tool/screenshots_ios.sh
|
||||
```
|
||||
|
||||
Fährt „iPhone 14 Plus“ (6,5″, 1284×2778) und „iPad Pro 13-inch (M5)“
|
||||
(13″, 2064×2752) nacheinander durch und legt die PNGs in
|
||||
`materials/screenshots/{ios/6-5-inch,ipados/13-inch}/` sowie als JPEG-Upload-Kopie
|
||||
(App Store Connect lehnt Alpha-Kanäle ab) in `screenshots/de-DE/` ab. Fehlende
|
||||
Simulatoren legt das Skript selbst an.
|
||||
|
||||
- Nur eine Größe: `PROFILES="iphone" tool/screenshots_ios.sh`
|
||||
- Andere Simulatoren: `IPHONE_SIM="iPhone 11 Pro Max" IPAD_SIM="…" tool/screenshots_ios.sh`
|
||||
|
||||
App und Schlüsselbund der verwendeten Simulatoren werden dabei zurückgesetzt.
|
||||
|
||||
## 2. Hochladen
|
||||
|
||||
fastlane installieren (`brew install fastlane`), in App Store Connect unter
|
||||
*Benutzer und Zugriff → Integrationen → App Store Connect API* einen Key mit
|
||||
Rolle *App-Manager* anlegen und als JSON ablegen:
|
||||
|
||||
```json
|
||||
{
|
||||
"key_id": "ABC123XYZ",
|
||||
"issuer_id": "00000000-0000-0000-0000-000000000000",
|
||||
"key": "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----",
|
||||
"in_house": false
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
export APP_STORE_CONNECT_API_KEY=/pfad/zu/app-store-connect-api-key.json
|
||||
cd ios
|
||||
fastlane upload_screenshots
|
||||
```
|
||||
|
||||
Die Screenshots landen in der aktuell bearbeitbaren App-Store-Version
|
||||
(„Vorbereitung für Einreichung“); vorhandene Screenshots werden ersetzt. Der
|
||||
API-Key wird **nicht** eingecheckt (siehe `.gitignore`).
|
||||
@@ -32,6 +32,8 @@ class DemoMarianumConnect {
|
||||
return DemoTimetable.timegrid().result.map((e) => e.toJson()).toList();
|
||||
case 'timetable/holidays':
|
||||
return DemoTimetable.holidays().result.map((e) => e.toJson()).toList();
|
||||
case 'timetable/custom-events':
|
||||
return DemoTimetable.customEvents().toJson();
|
||||
case 'holidays':
|
||||
return DemoHolidays.upcoming().map((e) => e.toJson()).toList();
|
||||
case 'users/search':
|
||||
|
||||
|
Before Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 596 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 604 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 544 KiB |
|
Before Width: | Height: | Size: 490 KiB |
@@ -8,6 +8,7 @@ import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_school
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
|
||||
import 'package:marianum_mobile/api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import 'package:marianum_mobile/api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
|
||||
/// The demo interceptor answers MarianumConnect calls with these bodies; each
|
||||
/// must round-trip through the same parser the real query uses. A shape
|
||||
@@ -57,6 +58,17 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
// Eigener Case nötig: sonst greift der timetable/<type>/<id>-Fallback und
|
||||
// liefert eine Woche statt `{events: [...]}` (iOS-Widget-Refresh crashte).
|
||||
test('timetable/custom-events', () {
|
||||
expect(
|
||||
() => GetCustomTimetableEventResponse.fromJson(
|
||||
asMap('timetable/custom-events'),
|
||||
),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
|
||||
test('holidays → non-empty list of McHoliday', () {
|
||||
final holidays = asList('holidays').map(McHoliday.fromJson).toList();
|
||||
expect(holidays, isNotEmpty);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# App-Store-Screenshots über den Demo-Login aufnehmen – iOS-Gegenstück zu
|
||||
# tool/screenshots.sh.
|
||||
#
|
||||
# Fährt denselben integration_test-Lauf (integration_test/screenshot_test.dart)
|
||||
# per flutter drive auf je einem iOS-Simulator pro Formfaktor: iPhone 6,5″
|
||||
# (iPhone 14 Plus, 1284×2778) und iPad 13″ (iPad Pro 13-inch, 2064×2752). Die
|
||||
# Simulatoren liefern die Frames direkt in der Zielauflösung, eine
|
||||
# Größenumstellung wie per `adb shell wm size` ist nicht nötig. Fehlt ein
|
||||
# Simulator, wird er mit gleichnamigem Gerätetyp angelegt.
|
||||
#
|
||||
# Ablage:
|
||||
# - kanonisch (versioniert): materials/screenshots/{ios/6-5-inch,ipados/13-inch}/
|
||||
# - Kopie für den Upload: ios/fastlane/screenshots/de-DE/<profil>_<name>.jpg
|
||||
# (deliver ordnet die Bilder anhand der Pixelgröße dem Gerät zu; das Präfix
|
||||
# verhindert nur Namenskollisionen zwischen iPhone und iPad.)
|
||||
# Danach: `cd ios && fastlane upload_screenshots`.
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - Xcode mit iOS-Simulator-Runtime; Simulatoren (Namen per IPHONE_SIM/IPAD_SIM
|
||||
# überschreibbar; Name = Gerätetyp aus `xcrun simctl list devicetypes`)
|
||||
# - flutter im PATH
|
||||
#
|
||||
# Achtung: Pro Simulator werden App und Schlüsselbund zurückgesetzt, damit der
|
||||
# Lauf garantiert mit dem Demo-Login startet und keine echte Sitzung zeigt.
|
||||
# Deshalb eigene Simulatoren verwenden, nicht den für die Entwicklung.
|
||||
#
|
||||
# Aufruf (vom Client-Projektwurzelverzeichnis):
|
||||
# tool/screenshots_ios.sh # debug-Build
|
||||
# BUILD_MODE=profile tool/screenshots_ios.sh
|
||||
# PROFILES="iphone" tool/screenshots_ios.sh # nur eine Größe
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
BUILD_MODE="${BUILD_MODE:-debug}"
|
||||
FASTLANE_DIR="ios/fastlane/screenshots/de-DE"
|
||||
BUNDLE_ID="eu.mhsl.marianum.mobile.client"
|
||||
IPHONE_SIM="${IPHONE_SIM:-iPhone 14 Plus}"
|
||||
IPAD_SIM="${IPAD_SIM:-iPad Pro 13-inch (M5)}"
|
||||
PROFILES="${PROFILES:-iphone ipad}"
|
||||
|
||||
# UDID des ersten verfügbaren Simulators mit exakt diesem Namen.
|
||||
sim_udid() {
|
||||
xcrun simctl list devices available -j |
|
||||
/usr/bin/python3 -c '
|
||||
import json, sys
|
||||
name = sys.argv[1]
|
||||
for devices in json.load(sys.stdin)["devices"].values():
|
||||
for d in devices:
|
||||
if d["name"] == name:
|
||||
print(d["udid"]); sys.exit(0)
|
||||
sys.exit(1)' "$1"
|
||||
}
|
||||
|
||||
BOOTED_BY_US=()
|
||||
cleanup() {
|
||||
for udid in ${BOOTED_BY_US[@]+"${BOOTED_BY_US[@]}"}; do
|
||||
xcrun simctl status_bar "$udid" clear >/dev/null 2>&1 || true
|
||||
xcrun simctl shutdown "$udid" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$FASTLANE_DIR"
|
||||
|
||||
for profile in $PROFILES; do
|
||||
case "$profile" in
|
||||
iphone) sim="$IPHONE_SIM"; materials_dir="materials/screenshots/ios/6-5-inch" ;;
|
||||
ipad) sim="$IPAD_SIM"; materials_dir="materials/screenshots/ipados/13-inch" ;;
|
||||
*) echo "Unbekanntes Profil '$profile' (erlaubt: iphone ipad)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
udid="$(sim_udid "$sim")" || {
|
||||
echo "Simulator '$sim' fehlt, lege ihn an…"
|
||||
udid="$(xcrun simctl create "$sim" "$sim")"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Profil '$profile' ($sim, $udid) -> $materials_dir ==="
|
||||
mkdir -p "$materials_dir"
|
||||
rm -f "$materials_dir"/*.png "$FASTLANE_DIR/${profile}_"*.jpg
|
||||
|
||||
if ! xcrun simctl list devices | grep -q "$udid) (Booted)"; then
|
||||
xcrun simctl boot "$udid"
|
||||
BOOTED_BY_US+=("$udid")
|
||||
fi
|
||||
xcrun simctl bootstatus "$udid" -b >/dev/null
|
||||
|
||||
# Apple-typische Statusleiste: 9:41, volles WLAN/Netz, Akku voll.
|
||||
xcrun simctl status_bar "$udid" override \
|
||||
--time "9:41" --dataNetwork wifi --wifiMode active --wifiBars 3 \
|
||||
--cellularMode active --cellularBars 4 --operatorName "" \
|
||||
--batteryState charged --batteryLevel 100
|
||||
|
||||
# Frische, ausgeloggte Sitzung: App löschen und Schlüsselbund leeren
|
||||
# (flutter_secure_storage überlebt auf iOS eine Deinstallation).
|
||||
xcrun simctl uninstall "$udid" "$BUNDLE_ID" >/dev/null 2>&1 || true
|
||||
xcrun simctl keychain "$udid" reset >/dev/null 2>&1 || true
|
||||
|
||||
SCREENSHOT_OUT_DIR="$materials_dir" flutter drive \
|
||||
-d "$udid" \
|
||||
--driver=test_driver/integration_test.dart \
|
||||
--target=integration_test/screenshot_test.dart \
|
||||
--"$BUILD_MODE" \
|
||||
--no-dds
|
||||
|
||||
# Kanonische Bilder mit Profil-Präfix in den Fastlane-Upload-Ordner spiegeln.
|
||||
# Als JPEG, weil App Store Connect Screenshots mit Alpha-Kanal ablehnt und
|
||||
# takeScreenshot RGBA-PNGs liefert.
|
||||
for f in "$materials_dir"/*.png; do
|
||||
sips -s format jpeg -s formatOptions best "$f" \
|
||||
--out "$FASTLANE_DIR/${profile}_$(basename "${f%.png}").jpg" >/dev/null
|
||||
done
|
||||
echo "Profil '$profile' fertig: $(ls -1 "$materials_dir"/*.png 2>/dev/null | wc -l | tr -d ' ') Screenshots"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Kanonische Screenshots: materials/screenshots/{ios/6-5-inch,ipados/13-inch}/"
|
||||
echo "Upload: cd ios && fastlane upload_screenshots"
|
||||