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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ef1d881967
commit
a1d64ec813
@@ -0,0 +1,183 @@
|
||||
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}');
|
||||
}
|
||||
Reference in New Issue
Block a user