Files
dmc/app/test/widgets/swipe_value_input_test.dart
Constantin LeueandClaude Sonnet 5 f25c312697 Add swipe gestures to drone profile editor (vertical=big, horizontal=small step)
Ersetzt die einfachen Zahlen-TextFormFields im Drohnenprofil-Editor durch
tastaturlose Swipe-Chips (Doku 3.5, HTML-Demonstrator: SwipeValueInput) -
vertikales Ziehen aendert den Wert in grossen Schritten (stepBig),
horizontales Ziehen in kleinen Schritten (stepSmall). Die Zugrichtung wird
beim ersten Ueberschreiten einer 8px-Schwelle einmalig festgelegt und
bleibt fuer den Rest der Geste gesperrt, damit eine leicht schraege Geste
nicht zwischen den beiden Schrittweiten hin- und herspringt. Min/Max/
Schrittweiten je Feld 1:1 aus DRONE_EDITOR_GROUPS im HTML-Demonstrator
uebernommen. Zusaetzlich ein "modified"-Punkt neben dem Label, sobald ein
Wert vom Ausgangswert beim Oeffnen des Editors abweicht (HTML: .modified-
dot/.chip-modified).

lib/ui/widgets/swipe_value_input.dart: neuer, wiederverwendbarer Chip.

Gesten-Konflikt mit der scrollbaren Formular-Liste (wichtigster Teil
dieser Aenderung): Die Chips sitzen in einer vertikal scrollbaren
ListView (elf Felder in Gruppen passen nicht auf einen Bildschirm). Ein
GestureDetector.onPan* auf dem Chip verlor dabei auf einem echten Geraet
durchgehend gegen die eigene Scroll-Geste der ListView - onPanStart/
onPanUpdate feuerten nie, weder fuer vertikale noch horizontale Zuege
(mit Debug-Prints auf dem Pixel_10a-Emulator verifiziert). Behoben durch:
- SwipeValueInput nutzt jetzt rohe Pointer-Events (Listener statt
  GestureDetector.onPan*) - diese werden unabhaengig vom Gesture-Arena-
  Ausgang immer zugestellt.
- Ein neuer onDragActiveChanged-Callback informiert das Elternwidget,
  solange eine Geste aktiv ist; DroneProfileEditor sperrt darueber die
  ListView (NeverScrollableScrollPhysics) fuer die Dauer des Ziehens, so
  dass die Liste waehrenddessen nicht mitscrollt.

Getestet: 7 Widget-Tests fuer SwipeValueInput (grosse/kleine Schritte,
Richtungswechsel, Klemmen auf min/max, Rundung auf Nachkommastellen,
"modified"-Punkt, sowie ein Regressionstest fuer exakt dieses Szenario -
Chip innerhalb einer scrollbaren ListView). Alle 117 Tests sowie flutter
analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert:
vertikaler Zug auf "Min (stall)" aendert den Wert in 10er-Schritten ohne
die Liste zu scrollen, horizontaler Zug auf "Cruise" in 1er-Schritten -
beide mit korrekt aufleuchtendem "modified"-Indikator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 21:11:51 +02:00

145 lines
4.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dmc_app/ui/widgets/swipe_value_input.dart';
void main() {
Widget wrap({
required double value,
required double initialValue,
required void Function(double) onChanged,
double min = 0,
double max = 200,
double stepBig = 10,
double stepSmall = 1,
int decimals = 0,
}) {
return MaterialApp(
home: Scaffold(
body: Center(
child: SwipeValueInput(
label: 'Test',
value: value,
initialValue: initialValue,
min: min,
max: max,
stepBig: stepBig,
stepSmall: stepSmall,
decimals: decimals,
unit: 'm',
onChanged: onChanged,
),
),
),
);
}
testWidgets('vertikales Ziehen aendert den Wert in grossen Schritten (stepBig)',
(tester) async {
final changes = <double>[];
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: changes.add));
// 3 Schritte a 14px nach oben -> +3*stepBig(10) = +30.
await tester.drag(find.byType(SwipeValueInput), const Offset(0, -42));
await tester.pump();
expect(changes, isNotEmpty);
expect(changes.last, 80);
});
testWidgets('horizontales Ziehen aendert den Wert in kleinen Schritten (stepSmall)',
(tester) async {
final changes = <double>[];
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: changes.add));
// 3 Schritte a 14px nach rechts -> +3*stepSmall(1) = +3.
await tester.drag(find.byType(SwipeValueInput), const Offset(42, 0));
await tester.pump();
expect(changes, isNotEmpty);
expect(changes.last, 53);
});
testWidgets('nach links/unten ziehen verringert den Wert', (tester) async {
final changes = <double>[];
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: changes.add));
await tester.drag(find.byType(SwipeValueInput), const Offset(0, 42));
await tester.pump();
expect(changes.last, 20);
});
testWidgets('klemmt auf max, wenn die grosse Schrittweite darueber hinausschiesst',
(tester) async {
final changes = <double>[];
await tester.pumpWidget(wrap(value: 190, initialValue: 190, onChanged: changes.add));
await tester.drag(find.byType(SwipeValueInput), const Offset(0, -140));
await tester.pump();
expect(changes.last, 200);
});
testWidgets('rundet auf die konfigurierte Nachkommastellenzahl', (tester) async {
final changes = <double>[];
await tester.pumpWidget(wrap(
value: 1.0,
initialValue: 1.0,
stepSmall: 0.5,
decimals: 1,
onChanged: changes.add,
));
await tester.drag(find.byType(SwipeValueInput), const Offset(14, 0));
await tester.pump();
expect(changes.last, 1.5);
});
testWidgets(
'aendert den Wert auch innerhalb einer scrollbaren Liste (Regression: '
'GestureDetector.onPan* verlor frueher gegen die Scroll-Geste der Liste)',
(tester) async {
final changes = <double>[];
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ListView(
children: [
const SizedBox(height: 400),
SwipeValueInput(
label: 'Test',
value: 50,
initialValue: 50,
min: 0,
max: 200,
stepBig: 10,
stepSmall: 1,
unit: 'm',
onChanged: changes.add,
),
const SizedBox(height: 400),
],
),
),
));
await tester.drag(find.byType(SwipeValueInput), const Offset(0, -42));
await tester.pump();
expect(changes, isNotEmpty);
expect(changes.last, 80);
});
testWidgets('zeigt den "modified"-Punkt nur, wenn der Wert vom Ausgangswert abweicht',
(tester) async {
await tester.pumpWidget(wrap(value: 50, initialValue: 50, onChanged: (_) {}));
var opacity = tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity));
expect(opacity.opacity, 0);
await tester.pumpWidget(wrap(value: 60, initialValue: 50, onChanged: (_) {}));
opacity = tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity));
expect(opacity.opacity, 1);
});
}