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>
215 lines
6.3 KiB
Dart
215 lines
6.3 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|