import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:hl_lieferservice/data/cache/attachment_cache.dart'; import 'package:hl_lieferservice/domain/entity/address.dart'; import 'package:hl_lieferservice/domain/entity/car.dart'; import 'package:hl_lieferservice/domain/entity/customer.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/payment_method.dart'; import 'package:hl_lieferservice/domain/entity/scan_progress.dart'; import 'package:hl_lieferservice/domain/entity/tour.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; import 'package:hl_lieferservice/domain/repository/cars_repository.dart'; import 'package:hl_lieferservice/domain/repository/payment_methods_repository.dart'; import 'package:hl_lieferservice/domain/repository/tour_repository.dart'; import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart'; import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart'; import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart'; import 'package:hl_lieferservice/feature/cars/bloc/cars_event.dart'; import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/phase_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_date_cubit.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart'; import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_sort_page.dart'; import 'package:hl_lieferservice/feature/delivery/overview/service/phase_service.dart'; import 'package:hl_lieferservice/feature/feature_flags/feature_flags.dart'; import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; import 'package:hl_lieferservice/widget/app_message.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart'; const _fehler = 'Das hat nicht geklappt'; const _address = Address( street: 'Konstantinplatz', houseNumber: '3', postalCode: '41849', city: 'Wassenberg', country: 'DE', ); Delivery _delivery(String id, int sortOrder) => Delivery( id: id, tourId: 't1', customerId: 'c1', contactPersonIds: const [], deliveryAddressSnapshot: _address, erpBelegartId: 24, erpBelegnummer: 'V-$id', state: DeliveryState.active, sortOrder: sortOrder, prepaidAmount: 20, paymentMethodId: 'm-cash', items: [ DeliveryItem( id: 'i-$id', deliveryId: id, articleId: 'a1', warehouseId: 'w1', belegzeilenNr: 1, requiredQuantity: 2, unitPrice: 50, scanProgress: ScanProgress( status: ScanStatus.done, scannedQuantity: 2, lastUpdatedAt: DateTime(2026), ), ), ], ); TourDetails _details() => TourDetails( tour: Tour( id: 't1', accountId: 414, date: DateTime(2026, 9, 25), syncedAt: DateTime(2026), ), deliveries: [_delivery('d1', 1), _delivery('d2', 2)], customers: const { 'c1': Customer( id: 'c1', name: 'Cremer', erpCustomerId: 7843068, address: _address, ), }, contacts: const {}, articles: const {}, warehouses: const {}, notesByDeliveryId: const {}, creditsByDeliveryId: const {}, paymentsByDeliveryId: const {}, services: const [], serviceValuesByDeliveryId: const {}, contactSourcesByDeliveryId: const {}, contactChannelsBySourceId: const {}, ); class _Tours implements TourRepository { @override Future getMyTourDetails({DateTime? date}) async => _details(); @override Future> setDeliveryOrder({ required String tourId, required List orderedDeliveryIds, }) async => throw const TourRepositoryException(_fehler); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _Cars implements CarsRepository { @override Future> listMine({bool includeInactive = false}) async => const []; @override Future create({required String plate}) async => throw const CarsRepositoryException(_fehler); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _Methods implements PaymentMethodsRepository { @override Future> list({bool includeInactive = false}) async => const []; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _NoCache implements AttachmentCache { @override Future retainOnly(Set ids) async {} @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } /// Baut [home] wie in der App: Theme, Enforcer und alle benötigten Blocs. Future _pump( WidgetTester tester, Widget Function(BuildContext) home, ) async { // Höhe eines iPhones — daran hängt die Überdeckung. Breiter als das Gerät, // weil die Testschrift deutlich breiter läuft als die echte. tester.view.physicalSize = const Size(2400, 2556); tester.view.devicePixelRatio = 3; addTearDown(tester.view.reset); SharedPreferences.setMockInitialValues({}); final opBloc = OperationBloc(); final tourBloc = TourBloc( tourRepository: _Tours(), opBloc: opBloc, attachmentCache: _NoCache(), )..add(const LoadTour()); final carsBloc = CarsBloc(repository: _Cars(), opBloc: opBloc) ..add(const CarLoad()); await tester.pumpWidget( MultiBlocProvider( providers: [ RepositoryProvider.value(value: const FeatureFlags({})), BlocProvider.value(value: opBloc), BlocProvider.value(value: tourBloc), BlocProvider.value(value: carsBloc), BlocProvider(create: (_) => TourDateCubit()), BlocProvider(create: (_) => PhaseBloc(phaseService: PhaseService())), BlocProvider( create: (_) => CarSelectBloc( repository: CarSelectionRepository(environmentId: 'test'), carsRepository: _Cars(), ), ), BlocProvider( create: (_) => PaymentMethodsCubit(repository: _Methods()), ), ], child: MaterialApp( theme: ThemeData(snackBarTheme: appSnackBarTheme), builder: (context, child) => OperationViewEnforcer(child: child!), home: Builder(builder: home), ), ), ); await tester.pumpAndSettle(); return opBloc; } /// Lässt die 4-s-Anzeige und das Nachlaufen des OperationBloc verstreichen. Future _meldungAblaufenLassen(WidgetTester tester) async { await tester.pump(const Duration(seconds: 6)); await tester.pumpAndSettle(); } void _erwarteFrei(WidgetTester tester, Finder bedienelement) { expect(bedienelement, findsOneWidget); final meldung = tester.getRect(find.byType(SnackBar)); final rect = tester.getRect(bedienelement); expect( meldung.overlaps(rect), isFalse, reason: 'Meldung $meldung verdeckt Bedienelement $rect', ); } Future _zeigeFehler(WidgetTester tester, OperationBloc opBloc) async { opBloc.add(FailOperation(message: _fehler)); await tester.pumpAndSettle(); expect(find.text(_fehler), findsOneWidget); } void main() { group('Meldungen verdecken keine Bedienelemente', () { testWidgets('Fahrzeugverwaltung: Plus-Button', (tester) async { final opBloc = await _pump(tester, (_) => const CarManagementPage()); await _zeigeFehler(tester, opBloc); _erwarteFrei(tester, find.byType(FloatingActionButton)); await _meldungAblaufenLassen(tester); }); testWidgets('Lieferdetail: Zurück, Weiter und die Step-Aktionen', ( tester, ) async { final opBloc = await _pump( tester, (_) => const DeliveryDetail(deliveryId: 'd1'), ); await _zeigeFehler(tester, opBloc); _erwarteFrei(tester, find.text('Zurück')); _erwarteFrei(tester, find.text('Weiter')); await _meldungAblaufenLassen(tester); // Step „Notizen": „Hinzufügen" schwebt über der Leiste. await tester.tap(find.text('Weiter')); await tester.pumpAndSettle(); await _zeigeFehler(tester, opBloc); _erwarteFrei(tester, find.text('Hinzufügen')); _erwarteFrei(tester, find.text('Weiter')); await _meldungAblaufenLassen(tester); // Über Artikel und Services zum Step „Zahlung". for (var i = 0; i < 3; i++) { await tester.tap(find.text('Weiter')); await tester.pumpAndSettle(); } await _zeigeFehler(tester, opBloc); _erwarteFrei(tester, find.text('Zahlung abwickeln')); await _meldungAblaufenLassen(tester); }); }); group('Meldungen stauen sich nicht', () { testWidgets('eine neue Meldung ersetzt die alte, auch mehrere schnell', ( tester, ) async { await _pump(tester, (_) => const Scaffold()); final context = tester.element(find.byType(Scaffold)); context.showAppMessage('eins'); await tester.pump(); context.showAppMessage('zwei'); context.showAppMessage('drei'); await tester.pumpAndSettle(const Duration(milliseconds: 100)); expect(find.text('drei'), findsOneWidget); expect(find.text('eins'), findsNothing); expect(find.text('zwei'), findsNothing); await tester.pump(const Duration(seconds: 5)); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsNothing); }); testWidgets('auch eine Meldung mit Aktion verschwindet von selbst', ( tester, ) async { await _pump(tester, (_) => const Scaffold()); tester .element(find.byType(Scaffold)) .showAppMessage( _fehler, action: SnackBarAction(label: 'Erneut', onPressed: () {}), ); await tester.pumpAndSettle(); expect(find.text(_fehler), findsOneWidget); await tester.pump(const Duration(seconds: 5)); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsNothing); }); }); testWidgets('Reihenfolge-Fehler erscheint genau einmal, auch beim zweiten Mal', (tester) async { await _pump( tester, (_) => const DeliverySortPage(selectedCarId: 'car-1'), ); for (var versuch = 0; versuch < 2; versuch++) { await tester.tap(find.text('Reihenfolge bestätigen')); await tester.pumpAndSettle(); expect(find.text(_fehler), findsOneWidget); // Eine zweite Meldung säße in der Warteschlange und käme jetzt. await _meldungAblaufenLassen(tester); expect(find.text(_fehler), findsNothing); } }); }