Files
pyramid/integration_test/mobile_ui_screens_test.dart
T
Bernd SteckmeisterandClaude Opus 5.5 a1d64ec813 feat: Handy - Platz fuer Statusleiste, randlose Bild-Vollansicht mit Gesten, groessere Medien im Chat
Bernds Beta-Feedback:
- main.dart: edgeToEdge statt immersiveSticky (Statusleiste schob sich ueber
  den Inhalt); MobileShell mit SafeArea + Statusleisten-Symbolen passend
  zum Theme, Hintergrund bg1 = Kopfzeilenfarbe.
- MediaViewer: Handy randlos auf Schwarz (vorher 24-px-Rand, halbtransparent);
  Tippen neben das Bild schliesst, aufs Bild blendet Leiste ein/aus; Wischen
  hoch/runter schliesst (nur ohne Zoom, ueber InteractiveViewer-Callbacks,
  kein Gestenkonflikt mit Zwei-Finger-Zoom); Doppeltipp zoomt an der Stelle.
- Chat: Bilder auf dem Handy bis halbe Bildschirmhoehe statt fest 320 px;
  Videos im echten Seitenverhaeltnis statt fest 16:9.
- UI-Durchlauf integration_test/mobile_ui_screens_test.dart (Handy-Format,
  Testprofil, Screenshots nach build/ui_screens/) + Testinhalte-Helfer
  test/live_ui_fixture_test.dart; scripts/test.ps1 -Ui.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 16:52:10 +02:00

184 lines
6.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:pyramid/main.dart' as app;
/// UI-Durchlauf im HANDY-Format mit der echten App (Windows-Build) und einem
/// TEST-Profil – erzeugt Screenshots unter build/ui_screens/.
///
/// Läuft nur mit gesetztem PYRAMID_PROFILE_DIR (eingeloggtes Testprofil, z. B.
/// pyramidtest1 unter %USERPROFILE%\.pyramid-autopilot\profile1) – nie gegen
/// Bernds echtes Profil. Testinhalte vorher mit
/// `test/live_ui_fixture_test.dart` anlegen (DM von pyramidtest2 mit Bildern).
///
/// $env:PYRAMID_PROFILE_DIR="$env:USERPROFILE\.pyramid-autopilot\profile1"
/// flutter test integration_test/mobile_ui_screens_test.dart -d windows
///
/// Achtung: Keine zweite Pyramid-Instanz auf demselben Profil laufen lassen
/// (zwei Clients auf einer olm-DB).
///
/// Simuliert wird ein typisches Handy: 1080×2340 px, Dichte 2,625
/// (≈ 411×891 dp), Statusleiste oben 24 dp, Gestenleiste unten 16 dp.
const _otherUser = 'pyramidtest2';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final profile = Platform.environment['PYRAMID_PROFILE_DIR'];
testWidgets(
'Handy-UI: Chatliste, Chat, Bild-Vollansicht',
(tester) async {
tester.view
..physicalSize = const Size(1080, 2340)
..devicePixelRatio = 2.625
..padding = const FakeViewPadding(top: 63, bottom: 42)
..viewPadding = const FakeViewPadding(top: 63, bottom: 42);
addTearDown(tester.view.reset);
app.main();
await _pumpUntil(
tester,
find.text(_otherUser),
timeout: const Duration(seconds: 90),
);
await _shot(tester, '01_chatliste');
await tester.tap(find.text(_otherUser).first);
await _pumpFor(tester, const Duration(seconds: 6));
await _shot(tester, '02_chat');
// Größtes Bild im Chat = eines der Testbilder; erst sichtbar scrollen.
Future<void> openViewer() async {
final image = _largestImage(tester);
expect(image, isNotNull, reason: 'kein Bild im Test-Chat gefunden');
await tester.ensureVisible(image!);
await _pumpFor(tester, const Duration(milliseconds: 600));
await tester.tap(image);
await _pumpFor(tester, const Duration(seconds: 2));
expect(
find.byType(InteractiveViewer),
findsOneWidget,
reason: 'Antippen des Bildes hat die Vollansicht nicht geöffnet',
);
}
await openViewer();
await _shot(tester, '03_vollansicht');
// Tippen NEBEN das Bild (ganz unten, unter dem Hochkant-Bild) schließt.
final size = tester.view.physicalSize / tester.view.devicePixelRatio;
await tester.tapAt(Offset(size.width / 2, size.height - 30));
await _pumpFor(tester, const Duration(seconds: 2));
await _shot(tester, '04_nach_tippen_daneben');
expect(
find.byType(InteractiveViewer),
findsNothing,
reason: 'Tippen neben das Bild hat die Vollansicht nicht geschlossen',
);
// Tippen AUFS Bild schließt NICHT (blendet nur die Leiste aus).
await openViewer();
await tester.tapAt(Offset(size.width / 2, size.height / 2));
await _pumpFor(tester, const Duration(seconds: 1));
expect(
find.byType(InteractiveViewer),
findsOneWidget,
reason: 'Tippen aufs Bild darf die Vollansicht nicht schließen',
);
// Doppeltipp zoomt hinein.
await tester.tapAt(Offset(size.width / 2, size.height / 2));
await tester.pump(const Duration(milliseconds: 80));
await tester.tapAt(Offset(size.width / 2, size.height / 2));
await _pumpFor(tester, const Duration(seconds: 1));
final viewer = tester.widget<InteractiveViewer>(
find.byType(InteractiveViewer),
);
expect(
viewer.transformationController!.value.getMaxScaleOnAxis(),
greaterThan(1.5),
reason: 'Doppeltipp hat nicht gezoomt',
);
await _shot(tester, '05_doppeltipp_zoom');
// Nochmal doppelt → zurück auf 1.
await tester.tapAt(Offset(size.width / 2, size.height / 2));
await tester.pump(const Duration(milliseconds: 80));
await tester.tapAt(Offset(size.width / 2, size.height / 2));
await _pumpFor(tester, const Duration(seconds: 1));
// Wischen nach unten schließt.
await tester.dragFrom(
Offset(size.width / 2, size.height / 2),
const Offset(0, 320),
);
await _pumpFor(tester, const Duration(seconds: 2));
expect(
find.byType(InteractiveViewer),
findsNothing,
reason: 'Wischen nach unten hat die Vollansicht nicht geschlossen',
);
await _shot(tester, '06_nach_wischen');
},
skip: profile == null,
timeout: const Timeout(Duration(minutes: 5)),
);
}
Finder? _largestImage(WidgetTester tester) {
Element? best;
var bestArea = 150.0 * 150.0;
for (final element in find.byType(Image).evaluate()) {
final box = element.renderObject;
if (box is! RenderBox || !box.hasSize) continue;
final area = box.size.width * box.size.height;
if (area > bestArea) {
bestArea = area;
best = element;
}
}
return best == null ? null : find.byElementPredicate((e) => e == best);
}
Future<void> _pumpFor(WidgetTester tester, Duration duration) async {
final end = DateTime.now().add(duration);
while (DateTime.now().isBefore(end)) {
await tester.pump(const Duration(milliseconds: 100));
}
}
Future<void> _pumpUntil(
WidgetTester tester,
Finder finder, {
required Duration timeout,
}) async {
final end = DateTime.now().add(timeout);
while (DateTime.now().isBefore(end)) {
await tester.pump(const Duration(milliseconds: 200));
if (finder.evaluate().isNotEmpty) return;
}
fail('Zeitüberschreitung: $finder nicht gefunden');
}
/// Speichert, was gerade auf dem (simulierten) Bildschirm ist.
Future<void> _shot(WidgetTester tester, String name) async {
await tester.pump();
final renderView = tester.binding.renderViews.first;
final layer = renderView.debugLayer! as OffsetLayer;
final physical = tester.view.physicalSize;
final image = await tester.binding.runAsync(
() => layer.toImage(Offset.zero & physical),
);
final bytes = await tester.binding.runAsync(
() => image!.toByteData(format: ui.ImageByteFormat.png),
);
final file = File('build/ui_screens/$name.png');
file.parent.createSync(recursive: true);
file.writeAsBytesSync(bytes!.buffer.asUint8List());
// ignore: avoid_print
print('Screenshot: ${file.path}');
}