fix: Fehlerbehandlung beim Laden und Meldungen überarbeiten
Reklamationen der Fahrer: Meldungen verdeckten Buttons, und nach dem Abschluss stand „Lieferung nicht in der Tour gefunden" ohne Ausweg da. Meldungen: - Erfolgs-SnackBars entfernt, bei Erfolg bleibt der Spinner - einheitliche, schwebende Meldungen statt Stau über den Buttons - Server-Rohtexte durch verständliche Texte ersetzt (api_failure_text) Laden: - fehlt eine geöffnete Lieferung, wird die Tour einmal neu geholt; die Meldung kommt nur nach erfolgreichem Abruf ohne die Lieferung - Endlos-Spinner auf Lieferdetail, Filial-Abholung und Beladen ersetzt durch Kopfzeile, Fehlergrund und „Erneut versuchen" - Tour wird nach einem Tageswechsel nachgeladen, offene Seiten bleiben dabei unberührt - Abschluss und Zuweisung überschreiben kein zwischenzeitliches Neuladen, alte Antworten drehen einen Abschluss nicht mehr zurück - leere Server-Antworten gelten als Fehler statt als „keine Daten" Anmeldung: - Token-Erneuerung wartet höchstens 12 s, der native Aufruf läuft weiter - App-Start ohne Netz zeigt einen Hinweis mit „Erneut versuchen" - Fehlerseite der Fahrzeugauswahl bietet „Abmelden" Außerdem Podfile.lock an die Plugins angeglichen. Tests für die neuen Fehlerpfade ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
214
test/tour_day_change_test.dart
Normal file
214
test/tour_day_change_test.dart
Normal file
@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:hl_lieferservice/data/cache/attachment_cache.dart';
|
||||
import 'package:hl_lieferservice/domain/entity/tour.dart';
|
||||
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||||
import 'package:hl_lieferservice/domain/repository/tour_repository.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/tour_day_watcher.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||
|
||||
final _day1 = DateTime(2026, 10, 30, 22);
|
||||
final _day2 = DateTime(2026, 10, 31, 7);
|
||||
|
||||
TourDetails _tourAm(DateTime date) => TourDetails(
|
||||
tour: Tour(id: 't-$date', accountId: 414, date: date, syncedAt: date),
|
||||
deliveries: const [],
|
||||
customers: const {},
|
||||
contacts: const {},
|
||||
articles: const {},
|
||||
warehouses: const {},
|
||||
notesByDeliveryId: const {},
|
||||
creditsByDeliveryId: const {},
|
||||
paymentsByDeliveryId: const {},
|
||||
services: const [],
|
||||
serviceValuesByDeliveryId: const {},
|
||||
contactSourcesByDeliveryId: const {},
|
||||
contactChannelsBySourceId: const {},
|
||||
);
|
||||
|
||||
/// Liefert die Tour zum aktuellen Testtag und zählt die Anfragen.
|
||||
class _Tours implements TourRepository {
|
||||
_Tours(this.now);
|
||||
|
||||
final DateTime Function() now;
|
||||
int loads = 0;
|
||||
Object? failure;
|
||||
|
||||
@override
|
||||
Future<TourDetails?> getMyTourDetails({DateTime? date}) async {
|
||||
loads++;
|
||||
final error = failure;
|
||||
if (error != null) throw error;
|
||||
return _tourAm(date ?? now());
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _NoCache implements AttachmentCache {
|
||||
@override
|
||||
Future<void> retainOnly(Set<String> ids) async {}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late DateTime now;
|
||||
late _Tours tours;
|
||||
late TourBloc bloc;
|
||||
|
||||
setUp(() {
|
||||
now = _day1;
|
||||
tours = _Tours(() => now);
|
||||
bloc = TourBloc(
|
||||
tourRepository: tours,
|
||||
opBloc: OperationBloc(),
|
||||
attachmentCache: _NoCache(),
|
||||
clock: () => now,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() => bloc.close());
|
||||
|
||||
Future<void> settle() =>
|
||||
Future<void>.delayed(const Duration(milliseconds: 5));
|
||||
|
||||
DateTime loadedDate() => (bloc.state as TourLoaded).details.tour.date;
|
||||
|
||||
group('Bloc', () {
|
||||
test('neuer Tag: lädt die Tour des neuen Tages', () async {
|
||||
bloc.add(const LoadTour());
|
||||
await settle();
|
||||
now = _day2;
|
||||
bloc.add(const RefreshTourIfDayChanged());
|
||||
await settle();
|
||||
|
||||
expect(loadedDate(), _day2);
|
||||
expect(tours.loads, 2);
|
||||
});
|
||||
|
||||
test('derselbe Tag: kein Request', () async {
|
||||
bloc.add(const LoadTour());
|
||||
await settle();
|
||||
now = _day1.add(const Duration(hours: 1));
|
||||
bloc.add(const RefreshTourIfDayChanged());
|
||||
await settle();
|
||||
|
||||
expect(tours.loads, 1);
|
||||
});
|
||||
|
||||
test('im Kalender gewählte Tour bleibt', () async {
|
||||
final gewaehlt = DateTime(2026, 10, 20);
|
||||
bloc.add(LoadTour.forDate(gewaehlt));
|
||||
await settle();
|
||||
now = _day2;
|
||||
bloc.add(const RefreshTourIfDayChanged());
|
||||
await settle();
|
||||
|
||||
expect(loadedDate(), gewaehlt);
|
||||
expect(tours.loads, 1);
|
||||
});
|
||||
|
||||
test('kein Netz: alte Tour bleibt, nächster Versuch lädt nach', () async {
|
||||
bloc.add(const LoadTour());
|
||||
await settle();
|
||||
now = _day2;
|
||||
tours.failure = const TourRepositoryException('Server nicht erreichbar');
|
||||
bloc.add(const RefreshTourIfDayChanged());
|
||||
await settle();
|
||||
|
||||
expect(loadedDate(), _day1);
|
||||
expect((bloc.state as TourLoaded).refreshError, isNotNull);
|
||||
|
||||
tours.failure = null;
|
||||
bloc.add(const RefreshTourIfDayChanged());
|
||||
await settle();
|
||||
|
||||
expect(loadedDate(), _day2);
|
||||
});
|
||||
|
||||
test('vor dem ersten Laden passiert nichts', () async {
|
||||
bloc.add(const RefreshTourIfDayChanged());
|
||||
await settle();
|
||||
|
||||
expect(tours.loads, 0);
|
||||
expect(bloc.state, isA<TourInitial>());
|
||||
});
|
||||
});
|
||||
|
||||
group('TourDayWatcher', () {
|
||||
Future<void> pumpWatcher(WidgetTester tester) async {
|
||||
await tester.runAsync(() async {
|
||||
bloc.add(const LoadTour());
|
||||
await settle();
|
||||
});
|
||||
await tester.pumpWidget(
|
||||
BlocProvider.value(
|
||||
value: bloc,
|
||||
child: MaterialApp(
|
||||
home: TourDayWatcher(
|
||||
child: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Lieferung')),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('öffnen'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> resume(WidgetTester tester) async {
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
await tester.runAsync(settle);
|
||||
}
|
||||
|
||||
testWidgets('Rückkehr in die App am neuen Tag lädt nach', (tester) async {
|
||||
await pumpWatcher(tester);
|
||||
// Der Test startet die App im Zustand „nicht aktiv", wie ein Wechsel.
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
|
||||
now = _day2;
|
||||
|
||||
await resume(tester);
|
||||
|
||||
expect(tours.loads, 2);
|
||||
expect(loadedDate(), _day2);
|
||||
});
|
||||
|
||||
testWidgets('offene Seite: erst beim Schließen wird nachgeladen',
|
||||
(tester) async {
|
||||
await pumpWatcher(tester);
|
||||
await tester.tap(find.text('öffnen'));
|
||||
await tester.pumpAndSettle();
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
|
||||
now = _day2;
|
||||
|
||||
await resume(tester);
|
||||
expect(tours.loads, 1, reason: 'Eingaben der offenen Seite schützen');
|
||||
expect(loadedDate(), _day1);
|
||||
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.runAsync(settle);
|
||||
|
||||
expect(tours.loads, 2);
|
||||
expect(loadedDate(), _day2);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user