- Handy: Chat gleitet federnd über die Chatliste (Liste bleibt aufgebaut, Scrollposition erhalten), beim Zurück wieder hinaus; Liste schon während des Hinausgleitens bedienbar. Mitgliederleiste gleitet herein. - Neue/gesendete Nachrichten ploppen weich auf (nur live, nicht beim Öffnen oder Nachladen), Reaktionen ploppen auf und hüpfen beim Hochzählen, Ungelesen-Zähler hüpft, Scroll-nach-unten-Knopf federt (Handy größer), Antwort-Leiste klappt federnd auf/zu, Senden-Pfeil ploppt, Vollansicht zoomt auf. Alles skaliert mit Einstellung „Animationen“ (0 % = aus). - Fix: Beim Raumwechsel blieben Antwort-Ziel, Auswahl und getippter Text am neuen Raum hängen (Antwort auf Raum A konnte in Raum B rausgehen). Jetzt eigenes Eingabefeld je Raum mit Entwurf im Speicher. - Antwort-Leiste: größeres Schließen-X auf dem Handy. - Tests: test/spring_motion_test.dart, UI-Durchlauf mit Übergängen. Co-Authored-By: Claude Opus 5.5 <[email protected]>
151 lines
5.1 KiB
Dart
151 lines
5.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pyramid/core/theme.dart';
|
|
import 'package:pyramid/layout/mobile_page_stack.dart';
|
|
import 'package:pyramid/widgets/spring_press.dart';
|
|
|
|
/// Handy-Übergang Chatliste ↔ Chat und die federnden Einblendungen.
|
|
void main() {
|
|
Widget host({required Widget? front, double motion = 1}) {
|
|
return PyramidTheme(
|
|
motion: motion,
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MobilePageStack(
|
|
base: const _Counter(key: ValueKey('liste')),
|
|
front: front,
|
|
background: const Color(0xFF101010),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('Chatliste behält ihren Zustand, Chat gleitet herein und hinaus',
|
|
(tester) async {
|
|
await tester.pumpWidget(host(front: null));
|
|
await tester.tap(find.byKey(const ValueKey('liste')));
|
|
await tester.pump();
|
|
expect(find.text('Zähler 1'), findsOneWidget);
|
|
|
|
// Chat öffnen: gleitet von rechts herein.
|
|
await tester.pumpWidget(host(front: const Text('Chat')));
|
|
await tester.pump(const Duration(milliseconds: 40));
|
|
final midX = tester.getTopLeft(find.text('Chat')).dx;
|
|
expect(midX, greaterThan(0), reason: 'Chat sollte noch hereingleiten');
|
|
await tester.pumpAndSettle();
|
|
expect(tester.getTopLeft(find.text('Chat')).dx, 0);
|
|
// Verdeckte Liste ist nicht mehr bedienbar, aber noch aufgebaut.
|
|
expect(find.text('Zähler 1', skipOffstage: false), findsOneWidget);
|
|
|
|
// Zurück: Chat gleitet hinaus und wird danach entfernt.
|
|
await tester.pumpWidget(host(front: null));
|
|
await tester.pump(const Duration(milliseconds: 40));
|
|
expect(find.text('Chat'), findsOneWidget, reason: 'noch beim Hinausgleiten');
|
|
// Schon während des Hinausgleitens ist die Liste wieder bedienbar.
|
|
await tester.tapAt(const Offset(20, 300));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Zähler 2'), findsOneWidget);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Chat'), findsNothing);
|
|
// Zustand der Liste ist erhalten geblieben.
|
|
await tester.tap(find.byKey(const ValueKey('liste')));
|
|
await tester.pump();
|
|
expect(find.text('Zähler 3'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Animationen 0 %: sofortiger Wechsel ohne Gleiten', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(host(front: null, motion: 0));
|
|
await tester.pumpWidget(host(front: const Text('Chat'), motion: 0));
|
|
await tester.pump();
|
|
expect(tester.getTopLeft(find.text('Chat')).dx, 0);
|
|
await tester.pumpWidget(host(front: null, motion: 0));
|
|
await tester.pump();
|
|
expect(find.text('Chat'), findsNothing);
|
|
});
|
|
|
|
testWidgets('PopIn spielt nur, wenn verlangt, und endet unverändert', (
|
|
tester,
|
|
) async {
|
|
Widget pop(bool play) => PyramidTheme(
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: PopIn(play: play, child: const Text('Neu')),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(pop(true));
|
|
await tester.pump(const Duration(milliseconds: 30));
|
|
final opacity = tester.widget<Opacity>(
|
|
find.ancestor(of: find.text('Neu'), matching: find.byType(Opacity)),
|
|
);
|
|
expect(opacity.opacity, lessThan(1));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
tester
|
|
.widget<Opacity>(
|
|
find.ancestor(of: find.text('Neu'), matching: find.byType(Opacity)),
|
|
)
|
|
.opacity,
|
|
1,
|
|
);
|
|
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pumpWidget(pop(false));
|
|
expect(
|
|
find.ancestor(of: find.text('Neu'), matching: find.byType(Opacity)),
|
|
findsNothing,
|
|
);
|
|
});
|
|
|
|
testWidgets('SpringBump hüpft nur beim Hochzählen', (tester) async {
|
|
Widget bump(int v) => PyramidTheme(
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(child: SpringBump(value: v, child: Text('$v'))),
|
|
),
|
|
);
|
|
double scale() => tester
|
|
.widget<Transform>(
|
|
find.ancestor(
|
|
of: find.byType(Text),
|
|
matching: find.byType(Transform),
|
|
),
|
|
)
|
|
.transform
|
|
.storage[0]; // x-Skalierung (z bleibt bei Transform.scale immer 1)
|
|
|
|
await tester.pumpWidget(bump(1));
|
|
expect(scale(), 1);
|
|
await tester.pumpWidget(bump(2));
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
expect(scale(), lessThan(1), reason: 'Hochzählen soll hüpfen');
|
|
await tester.pumpAndSettle();
|
|
expect(scale(), closeTo(1, 0.001));
|
|
await tester.pumpWidget(bump(0));
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
expect(scale(), closeTo(1, 0.001), reason: 'Herunterzählen hüpft nicht');
|
|
});
|
|
}
|
|
|
|
class _Counter extends StatefulWidget {
|
|
const _Counter({super.key});
|
|
|
|
@override
|
|
State<_Counter> createState() => _CounterState();
|
|
}
|
|
|
|
class _CounterState extends State<_Counter> {
|
|
int _n = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => setState(() => _n++),
|
|
child: Center(child: Text('Zähler $_n')),
|
|
);
|
|
}
|